mirror of
https://github.com/johndoe6345789/WizardMerge.git
synced 2026-04-24 13:44:55 +00:00
Add Git CLI integration and enhance roadmap documentation
- Implement Git CLI wrapper module (git_cli.h/cpp) - Add branch creation support to PRController - Document semantic merging approaches in Phase 2 - Document SDG analysis implementation from research - Add Bitbucket and extensible platform support docs - Update Phase 1.5 to mark Git CLI integration complete - Add comprehensive tests for Git CLI operations Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
48
README.md
48
README.md
@@ -147,6 +147,16 @@ curl -X POST http://localhost:8080/api/pr/resolve \
|
||||
"pr_url": "https://gitlab.com/owner/repo/-/merge_requests/456",
|
||||
"api_token": "glpat-xxx"
|
||||
}'
|
||||
|
||||
# With branch creation (requires Git CLI)
|
||||
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",
|
||||
"create_branch": true,
|
||||
"branch_name": "wizardmerge-resolved-pr-123"
|
||||
}'
|
||||
```
|
||||
|
||||
The API will:
|
||||
@@ -155,7 +165,43 @@ The API will:
|
||||
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
|
||||
6. Optionally create a new branch with resolved changes (if `create_branch: true` and Git CLI available)
|
||||
7. Return merged content with conflict status
|
||||
|
||||
### Git CLI Integration
|
||||
|
||||
WizardMerge includes Git CLI integration for advanced workflows:
|
||||
|
||||
**Features:**
|
||||
- Clone repositories locally
|
||||
- Create and checkout branches
|
||||
- Stage and commit resolved changes
|
||||
- Push branches to remote repositories
|
||||
|
||||
**Branch Creation Workflow:**
|
||||
|
||||
When `create_branch: true` is set in the API request:
|
||||
1. Repository is cloned to a temporary directory
|
||||
2. New branch is created from the PR base branch
|
||||
3. Resolved files are written to the working directory
|
||||
4. Changes are staged and committed
|
||||
5. Branch path is returned in the response
|
||||
|
||||
**Requirements:**
|
||||
- Git CLI must be installed and available in system PATH
|
||||
- For pushing to remote, Git credentials must be configured (SSH keys or credential helpers)
|
||||
|
||||
**Example Response with Branch Creation:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"branch_created": true,
|
||||
"branch_name": "wizardmerge-resolved-pr-123",
|
||||
"branch_path": "/tmp/wizardmerge_pr_123_1234567890",
|
||||
"note": "Branch created successfully. Push to remote with: git -C /tmp/wizardmerge_pr_123_1234567890 push origin wizardmerge-resolved-pr-123",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
|
||||
160
ROADMAP.md
160
ROADMAP.md
@@ -77,13 +77,19 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me
|
||||
### 1.5 Git Integration
|
||||
**Priority: MEDIUM**
|
||||
|
||||
- [x] **Git CLI wrapper module** (`backend/include/wizardmerge/git/git_cli.h`)
|
||||
- Clone repositories
|
||||
- Create and checkout branches
|
||||
- Stage, commit, and push changes
|
||||
- Query repository status
|
||||
- Integrated into PR resolution workflow
|
||||
- [ ] Detect when running in Git repository
|
||||
- [ ] Read `.git/MERGE_HEAD` to identify conflicts
|
||||
- [ ] List all conflicted files
|
||||
- [ ] Mark files as resolved in Git
|
||||
- [ ] Launch from command line: `wizardmerge [file]`
|
||||
|
||||
**Deliverable**: `wizardmerge/git/` module and CLI enhancements
|
||||
**Deliverable**: `backend/src/git/` module and CLI enhancements ✓ (Partial)
|
||||
|
||||
---
|
||||
|
||||
@@ -93,18 +99,71 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me
|
||||
**Priority: HIGH**
|
||||
|
||||
- [ ] Semantic merge for common file types:
|
||||
- JSON: merge by key structure
|
||||
- YAML: preserve hierarchy
|
||||
- Package files: intelligent dependency merging
|
||||
- XML: structure-aware merging
|
||||
- **JSON**: Merge by key structure, preserve nested objects, handle array conflicts intelligently
|
||||
- Detect structural changes vs. value changes
|
||||
- Handle object key additions/deletions
|
||||
- Smart array merging (by ID fields when available)
|
||||
- **YAML**: Preserve hierarchy and indentation
|
||||
- Maintain comments and anchors
|
||||
- Detect schema-aware conflicts
|
||||
- Handle multi-document YAML files
|
||||
- **Package files**: Intelligent dependency merging
|
||||
- `package.json` (npm): Merge dependencies by semver ranges
|
||||
- `requirements.txt` (pip): Detect version conflicts
|
||||
- `go.mod`, `Cargo.toml`, `pom.xml`: Language-specific dependency resolution
|
||||
- Detect breaking version upgrades
|
||||
- **XML**: Structure-aware merging
|
||||
- Preserve DTD and schema declarations
|
||||
- Match elements by attributes (e.g., `id`)
|
||||
- Handle namespaces correctly
|
||||
- [ ] Language-aware merging (AST-based):
|
||||
- Python imports and functions
|
||||
- JavaScript/TypeScript modules
|
||||
- Java classes and methods
|
||||
- **Python**: Parse imports, function definitions, class hierarchies
|
||||
- Detect import conflicts and duplicates
|
||||
- Merge function/method definitions intelligently
|
||||
- Handle decorators and type hints
|
||||
- **JavaScript/TypeScript**: Module and export analysis
|
||||
- Merge import statements without duplicates
|
||||
- Handle named vs. default exports
|
||||
- Detect React component conflicts
|
||||
- **Java**: Class structure and method signatures
|
||||
- Merge method overloads
|
||||
- Handle package declarations
|
||||
- Detect annotation conflicts
|
||||
- **C/C++**: Header guards, include directives, function declarations
|
||||
- Merge `#include` directives
|
||||
- Detect macro conflicts
|
||||
- Handle namespace conflicts
|
||||
- [ ] SDG (System Dependence Graph) Analysis:
|
||||
- **Implementation based on research paper** (docs/PAPER.md)
|
||||
- Build dependency graphs at multiple levels:
|
||||
- **Text-level**: Line and block dependencies
|
||||
- **LLVM-IR level**: Data and control flow dependencies (for C/C++)
|
||||
- **AST-level**: Semantic dependencies (for all languages)
|
||||
- **Conflict Analysis**:
|
||||
- Detect true conflicts vs. false conflicts
|
||||
- Identify dependent code blocks affected by conflicts
|
||||
- Compute conflict impact radius
|
||||
- Suggest resolution based on dependency chains
|
||||
- **Features**:
|
||||
- 28.85% reduction in resolution time (per research)
|
||||
- Suggestions for >70% of conflicted blocks
|
||||
- Visual dependency graph in UI
|
||||
- Highlight upstream/downstream dependencies
|
||||
- **Implementation approach**:
|
||||
- Use tree-sitter for AST parsing
|
||||
- Integrate LLVM for IR analysis (C/C++ code)
|
||||
- Build dependency database per file
|
||||
- Cache analysis results for performance
|
||||
- [ ] Auto-resolution suggestions with confidence scores
|
||||
- Assign confidence based on SDG analysis
|
||||
- Learn from user's resolution patterns
|
||||
- Machine learning model for conflict classification
|
||||
- [ ] Learn from user's resolution patterns
|
||||
- Store resolution history
|
||||
- Pattern matching for similar conflicts
|
||||
- Suggest resolutions based on past behavior
|
||||
|
||||
**Deliverable**: `wizardmerge/algo/semantic/` module
|
||||
**Deliverable**: `backend/src/semantic/` module with SDG analysis engine
|
||||
|
||||
### 2.2 Enhanced Visualization
|
||||
**Priority: MEDIUM**
|
||||
@@ -115,6 +174,10 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me
|
||||
- [ ] Collapsible unchanged regions
|
||||
- [ ] Blame/history annotations
|
||||
- [ ] Conflict complexity indicator
|
||||
- [ ] **SDG visualization**:
|
||||
- Interactive dependency graph
|
||||
- Highlight conflicted nodes and their dependencies
|
||||
- Show data flow and control flow edges
|
||||
|
||||
**Deliverable**: Advanced QML components and visualization modes
|
||||
|
||||
@@ -126,8 +189,12 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me
|
||||
- [ ] Show syntax errors in real-time
|
||||
- [ ] Auto-formatting after resolution
|
||||
- [ ] Import/dependency conflict detection
|
||||
- [ ] **SDG-based suggestions**:
|
||||
- Use LSP for real-time dependency analysis
|
||||
- Validate resolution against type system
|
||||
- Suggest imports/references needed
|
||||
|
||||
**Deliverable**: `wizardmerge/lsp/` integration module
|
||||
**Deliverable**: `backend/src/lsp/` integration module
|
||||
|
||||
### 2.4 Multi-Frontend Architecture
|
||||
**Priority: HIGH**
|
||||
@@ -143,7 +210,76 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me
|
||||
|
||||
**Deliverable**: `wizardmerge/core/` (backend abstraction), `frontends/qt6/` (C++/Qt6), `frontends/web/` (Next.js)
|
||||
|
||||
### 2.5 Collaboration Features
|
||||
### 2.5 Additional Platform Support
|
||||
**Priority: MEDIUM**
|
||||
|
||||
- [ ] **Bitbucket** Pull Request support:
|
||||
- Bitbucket Cloud API integration
|
||||
- URL pattern: `https://bitbucket.org/workspace/repo/pull-requests/123`
|
||||
- Authentication via App passwords or OAuth
|
||||
- Support for Bitbucket Server (self-hosted)
|
||||
- [ ] **Azure DevOps** Pull Request support:
|
||||
- Azure DevOps REST API integration
|
||||
- URL pattern: `https://dev.azure.com/org/project/_git/repo/pullrequest/123`
|
||||
- Authentication via Personal Access Tokens
|
||||
- Support for on-premises Azure DevOps Server
|
||||
- [ ] **Gitea/Forgejo** support:
|
||||
- Self-hosted Git service integration
|
||||
- Compatible API with GitHub/GitLab patterns
|
||||
- Community-driven platforms
|
||||
- [ ] **Extensible Platform Pattern**:
|
||||
- **Abstract Git Platform Interface**:
|
||||
```cpp
|
||||
class GitPlatformAPI {
|
||||
virtual PullRequest fetch_pr_info() = 0;
|
||||
virtual std::vector<std::string> fetch_file_content() = 0;
|
||||
virtual bool create_comment() = 0;
|
||||
virtual bool update_pr_status() = 0;
|
||||
};
|
||||
```
|
||||
- **Platform Registry**:
|
||||
- Auto-detect platform from URL pattern
|
||||
- Plugin system for custom platforms
|
||||
- Configuration-based platform definitions
|
||||
- **Common API adapter layer**:
|
||||
- Normalize PR/MR data structures across platforms
|
||||
- Handle authentication differences (tokens, OAuth, SSH keys)
|
||||
- Abstract API versioning differences
|
||||
- **Implementation Guide** (for adding new platforms):
|
||||
1. Add URL regex pattern to `parse_pr_url()` in `git_platform_client.cpp`
|
||||
2. Add platform enum value to `GitPlatform` enum
|
||||
3. Implement API client functions for the platform
|
||||
4. Add platform-specific authentication handling
|
||||
5. Add unit tests for URL parsing and API calls
|
||||
6. Update documentation with examples
|
||||
- **Example: Adding Bitbucket**:
|
||||
```cpp
|
||||
// 1. Add to GitPlatform enum
|
||||
enum class GitPlatform { GitHub, GitLab, Bitbucket, Unknown };
|
||||
|
||||
// 2. Add URL pattern
|
||||
std::regex bitbucket_regex(
|
||||
R"((?:https?://)?bitbucket\.org/([^/]+)/([^/]+)/pull-requests/(\d+))"
|
||||
);
|
||||
|
||||
// 3. Implement API functions
|
||||
if (platform == GitPlatform::Bitbucket) {
|
||||
api_url = "https://api.bitbucket.org/2.0/repositories/" +
|
||||
owner + "/" + repo + "/pullrequests/" + pr_number;
|
||||
// Add Bearer token authentication
|
||||
headers = curl_slist_append(headers,
|
||||
("Authorization: Bearer " + token).c_str());
|
||||
}
|
||||
|
||||
// 4. Map Bitbucket response to PullRequest structure
|
||||
// Bitbucket uses different field names (e.g., "source" vs "head")
|
||||
pr.base_ref = root["destination"]["branch"]["name"].asString();
|
||||
pr.head_ref = root["source"]["branch"]["name"].asString();
|
||||
```
|
||||
|
||||
**Deliverable**: `backend/src/git/platform_registry.cpp` and platform-specific adapters
|
||||
|
||||
### 2.6 Collaboration Features
|
||||
**Priority: LOW**
|
||||
|
||||
- [ ] Add comments to conflicts
|
||||
@@ -154,7 +290,7 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me
|
||||
|
||||
**Deliverable**: Collaboration UI and sharing infrastructure
|
||||
|
||||
### 2.6 Testing & Quality
|
||||
### 2.7 Testing & Quality
|
||||
**Priority: HIGH**
|
||||
|
||||
- [ ] Comprehensive test suite for merge algorithms
|
||||
|
||||
@@ -14,6 +14,7 @@ find_package(CURL QUIET)
|
||||
# Library sources
|
||||
set(WIZARDMERGE_SOURCES
|
||||
src/merge/three_way_merge.cpp
|
||||
src/git/git_cli.cpp
|
||||
)
|
||||
|
||||
# Add git sources only if CURL is available
|
||||
@@ -67,7 +68,10 @@ endif()
|
||||
if(GTest_FOUND)
|
||||
enable_testing()
|
||||
|
||||
set(TEST_SOURCES tests/test_three_way_merge.cpp)
|
||||
set(TEST_SOURCES
|
||||
tests/test_three_way_merge.cpp
|
||||
tests/test_git_cli.cpp
|
||||
)
|
||||
|
||||
# Add github client tests only if CURL is available
|
||||
if(CURL_FOUND)
|
||||
|
||||
@@ -172,28 +172,38 @@ curl -X POST http://localhost:8080/api/merge \
|
||||
|
||||
### POST /api/pr/resolve
|
||||
|
||||
Resolve conflicts in a GitHub pull request.
|
||||
Resolve conflicts in a GitHub or GitLab pull/merge request.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"pr_url": "https://github.com/owner/repo/pull/123",
|
||||
"github_token": "ghp_xxx",
|
||||
"api_token": "ghp_xxx",
|
||||
"create_branch": true,
|
||||
"branch_name": "wizardmerge-resolved-pr-123"
|
||||
}
|
||||
```
|
||||
|
||||
**Request Fields:**
|
||||
- `pr_url` (required): Pull/merge request URL (GitHub or GitLab)
|
||||
- `api_token` (optional): API token for authentication (GitHub: `ghp_*`, GitLab: `glpat-*`)
|
||||
- `create_branch` (optional, default: false): Create a new branch with resolved changes
|
||||
- `branch_name` (optional): Custom branch name (auto-generated if not provided)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"pr_info": {
|
||||
"platform": "GitHub",
|
||||
"number": 123,
|
||||
"title": "Feature: Add new functionality",
|
||||
"base_ref": "main",
|
||||
"head_ref": "feature-branch",
|
||||
"mergeable": false
|
||||
"base_sha": "abc123...",
|
||||
"head_sha": "def456...",
|
||||
"mergeable": false,
|
||||
"mergeable_state": "dirty"
|
||||
},
|
||||
"resolved_files": [
|
||||
{
|
||||
@@ -206,21 +216,52 @@ Resolve conflicts in a GitHub pull request.
|
||||
],
|
||||
"total_files": 5,
|
||||
"resolved_count": 4,
|
||||
"failed_count": 0
|
||||
"failed_count": 0,
|
||||
"branch_created": true,
|
||||
"branch_name": "wizardmerge-resolved-pr-123",
|
||||
"branch_path": "/tmp/wizardmerge_pr_123_1234567890",
|
||||
"note": "Branch created successfully. Push to remote with: git -C /tmp/wizardmerge_pr_123_1234567890 push origin wizardmerge-resolved-pr-123"
|
||||
}
|
||||
```
|
||||
|
||||
**Example with curl:**
|
||||
```sh
|
||||
# Basic conflict resolution
|
||||
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"
|
||||
"api_token": "ghp_xxx"
|
||||
}'
|
||||
|
||||
# With branch creation
|
||||
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",
|
||||
"create_branch": true,
|
||||
"branch_name": "resolved-conflicts"
|
||||
}'
|
||||
```
|
||||
|
||||
**Note:** Requires libcurl to be installed. The GitHub token is optional for public repositories but required for private ones.
|
||||
**Git CLI Integration:**
|
||||
|
||||
When `create_branch: true` is specified:
|
||||
1. **Clone**: Repository is cloned to `/tmp/wizardmerge_pr_<number>_<timestamp>`
|
||||
2. **Branch**: New branch is created from the PR base branch
|
||||
3. **Resolve**: Merged files are written to the working directory
|
||||
4. **Commit**: Changes are staged and committed with message "Resolve conflicts for PR #<number>"
|
||||
5. **Response**: Branch path is returned for manual inspection or pushing
|
||||
|
||||
**Requirements for Branch Creation:**
|
||||
- Git CLI must be installed (`git --version` works)
|
||||
- Sufficient disk space for repository clone
|
||||
- Write permissions to `/tmp` directory
|
||||
|
||||
**Security Note:** Branch is created locally. To push to remote, configure Git credentials separately (SSH keys or credential helpers). Do not embed tokens in Git URLs.
|
||||
|
||||
**Note:** Requires libcurl to be installed. The API token is optional for public repositories but required for private ones.
|
||||
|
||||
## Deployment
|
||||
|
||||
|
||||
159
backend/include/wizardmerge/git/git_cli.h
Normal file
159
backend/include/wizardmerge/git/git_cli.h
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @file git_cli.h
|
||||
* @brief Git CLI wrapper for repository operations
|
||||
*
|
||||
* Provides C++ wrapper functions for Git command-line operations including
|
||||
* cloning, branching, committing, and pushing changes.
|
||||
*/
|
||||
|
||||
#ifndef WIZARDMERGE_GIT_CLI_H
|
||||
#define WIZARDMERGE_GIT_CLI_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace git {
|
||||
|
||||
/**
|
||||
* @brief Result of a Git operation
|
||||
*/
|
||||
struct GitResult {
|
||||
bool success;
|
||||
std::string output;
|
||||
std::string error;
|
||||
int exit_code;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Configuration for Git operations
|
||||
*/
|
||||
struct GitConfig {
|
||||
std::string user_name;
|
||||
std::string user_email;
|
||||
std::string auth_token; // For HTTPS authentication
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Clone a Git repository
|
||||
*
|
||||
* @param url Repository URL (HTTPS or SSH)
|
||||
* @param destination Local directory path
|
||||
* @param branch Optional specific branch to clone
|
||||
* @param depth Optional shallow clone depth (0 for full clone)
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult clone_repository(
|
||||
const std::string& url,
|
||||
const std::string& destination,
|
||||
const std::string& branch = "",
|
||||
int depth = 0
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Create and checkout a new branch
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param branch_name Name of the new branch
|
||||
* @param base_branch Optional base branch (defaults to current branch)
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult create_branch(
|
||||
const std::string& repo_path,
|
||||
const std::string& branch_name,
|
||||
const std::string& base_branch = ""
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Checkout an existing branch
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param branch_name Name of the branch to checkout
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult checkout_branch(
|
||||
const std::string& repo_path,
|
||||
const std::string& branch_name
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Stage files for commit
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param files Vector of file paths (relative to repo root)
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult add_files(
|
||||
const std::string& repo_path,
|
||||
const std::vector<std::string>& files
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Commit staged changes
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param message Commit message
|
||||
* @param config Optional Git configuration
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult commit(
|
||||
const std::string& repo_path,
|
||||
const std::string& message,
|
||||
const GitConfig& config = GitConfig()
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Push commits to remote repository
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param remote Remote name (default: "origin")
|
||||
* @param branch Branch name to push
|
||||
* @param force Force push if needed
|
||||
* @param config Optional Git configuration with auth token
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult push(
|
||||
const std::string& repo_path,
|
||||
const std::string& remote,
|
||||
const std::string& branch,
|
||||
bool force = false,
|
||||
const GitConfig& config = GitConfig()
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Get current branch name
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @return Current branch name, or empty optional on error
|
||||
*/
|
||||
std::optional<std::string> get_current_branch(const std::string& repo_path);
|
||||
|
||||
/**
|
||||
* @brief Check if a branch exists
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param branch_name Name of the branch to check
|
||||
* @return true if branch exists, false otherwise
|
||||
*/
|
||||
bool branch_exists(const std::string& repo_path, const std::string& branch_name);
|
||||
|
||||
/**
|
||||
* @brief Get repository status
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @return GitResult with status output
|
||||
*/
|
||||
GitResult status(const std::string& repo_path);
|
||||
|
||||
/**
|
||||
* @brief Check if Git is available in system PATH
|
||||
*
|
||||
* @return true if git command is available, false otherwise
|
||||
*/
|
||||
bool is_git_available();
|
||||
|
||||
} // namespace git
|
||||
} // namespace wizardmerge
|
||||
|
||||
#endif // WIZARDMERGE_GIT_CLI_H
|
||||
@@ -5,9 +5,11 @@
|
||||
|
||||
#include "PRController.h"
|
||||
#include "wizardmerge/git/git_platform_client.h"
|
||||
#include "wizardmerge/git/git_cli.h"
|
||||
#include "wizardmerge/merge/three_way_merge.h"
|
||||
#include <json/json.h>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
using namespace wizardmerge::controllers;
|
||||
using namespace wizardmerge::git;
|
||||
@@ -180,15 +182,114 @@ void PRController::resolvePR(
|
||||
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
|
||||
// Branch creation with Git CLI
|
||||
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)";
|
||||
|
||||
// Check if Git CLI is available
|
||||
if (!is_git_available()) {
|
||||
response["note"] = "Git CLI not available - branch creation skipped";
|
||||
} else {
|
||||
// Clone repository to temporary location
|
||||
std::string temp_dir = "/tmp/wizardmerge_pr_" + std::to_string(pr_number) + "_" +
|
||||
std::to_string(std::time(nullptr));
|
||||
|
||||
// Build repository URL
|
||||
std::string repo_url;
|
||||
if (platform == GitPlatform::GitHub) {
|
||||
repo_url = "https://github.com/" + owner + "/" + repo + ".git";
|
||||
} else if (platform == GitPlatform::GitLab) {
|
||||
std::string project_path = owner;
|
||||
if (!repo.empty()) {
|
||||
project_path += "/" + repo;
|
||||
}
|
||||
repo_url = "https://gitlab.com/" + project_path + ".git";
|
||||
}
|
||||
|
||||
// Clone the repository
|
||||
auto clone_result = clone_repository(repo_url, temp_dir, pr.base_ref);
|
||||
|
||||
if (!clone_result.success) {
|
||||
response["note"] = "Failed to clone repository: " + clone_result.error;
|
||||
} else {
|
||||
// Create new branch
|
||||
auto branch_result = create_branch(temp_dir, branch_name, pr.base_ref);
|
||||
|
||||
if (!branch_result.success) {
|
||||
response["note"] = "Failed to create branch: " + branch_result.error;
|
||||
std::filesystem::remove_all(temp_dir);
|
||||
} else {
|
||||
// Write resolved files
|
||||
bool all_files_written = true;
|
||||
for (const auto& file : resolved_files_array) {
|
||||
if (file.isMember("merged_content") && file["merged_content"].isArray()) {
|
||||
std::string file_path = temp_dir + "/" + file["filename"].asString();
|
||||
|
||||
// Create parent directories
|
||||
std::filesystem::path file_path_obj(file_path);
|
||||
std::filesystem::create_directories(file_path_obj.parent_path());
|
||||
|
||||
// Write merged content
|
||||
std::ofstream out_file(file_path);
|
||||
if (out_file.is_open()) {
|
||||
for (const auto& line : file["merged_content"]) {
|
||||
out_file << line.asString() << "\n";
|
||||
}
|
||||
out_file.close();
|
||||
} else {
|
||||
all_files_written = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!all_files_written) {
|
||||
response["note"] = "Failed to write some resolved files";
|
||||
std::filesystem::remove_all(temp_dir);
|
||||
} else {
|
||||
// Stage and commit changes
|
||||
std::vector<std::string> file_paths;
|
||||
for (const auto& file : resolved_files_array) {
|
||||
if (file.isMember("filename")) {
|
||||
file_paths.push_back(file["filename"].asString());
|
||||
}
|
||||
}
|
||||
|
||||
auto add_result = add_files(temp_dir, file_paths);
|
||||
if (!add_result.success) {
|
||||
response["note"] = "Failed to stage files: " + add_result.error;
|
||||
std::filesystem::remove_all(temp_dir);
|
||||
} else {
|
||||
GitConfig git_config;
|
||||
git_config.user_name = "WizardMerge Bot";
|
||||
git_config.user_email = "wizardmerge@example.com";
|
||||
git_config.auth_token = api_token;
|
||||
|
||||
std::string commit_message = "Resolve conflicts for PR #" + std::to_string(pr_number);
|
||||
auto commit_result = commit(temp_dir, commit_message, git_config);
|
||||
|
||||
if (!commit_result.success) {
|
||||
response["note"] = "Failed to commit changes: " + commit_result.error;
|
||||
std::filesystem::remove_all(temp_dir);
|
||||
} else {
|
||||
response["branch_created"] = true;
|
||||
response["branch_path"] = temp_dir;
|
||||
response["note"] = "Branch created successfully. Push to remote with: git -C " +
|
||||
temp_dir + " push origin " + branch_name;
|
||||
|
||||
// Note: Pushing requires authentication setup
|
||||
// For security, we don't push automatically with token in URL
|
||||
// Users should configure Git credentials or use SSH keys
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
|
||||
209
backend/src/git/git_cli.cpp
Normal file
209
backend/src/git/git_cli.cpp
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* @file git_cli.cpp
|
||||
* @brief Implementation of Git CLI wrapper functions
|
||||
*/
|
||||
|
||||
#include "wizardmerge/git/git_cli.h"
|
||||
#include <cstdlib>
|
||||
#include <array>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace git {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Execute a shell command and capture output
|
||||
*/
|
||||
GitResult execute_command(const std::string& command) {
|
||||
GitResult result;
|
||||
result.exit_code = 0;
|
||||
|
||||
// Execute command and capture output
|
||||
std::array<char, 128> buffer;
|
||||
std::string output;
|
||||
|
||||
FILE* pipe = popen((command + " 2>&1").c_str(), "r");
|
||||
if (!pipe) {
|
||||
result.success = false;
|
||||
result.error = "Failed to execute command";
|
||||
result.exit_code = -1;
|
||||
return result;
|
||||
}
|
||||
|
||||
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
|
||||
output += buffer.data();
|
||||
}
|
||||
|
||||
int status = pclose(pipe);
|
||||
result.exit_code = WEXITSTATUS(status);
|
||||
result.success = (result.exit_code == 0);
|
||||
result.output = output;
|
||||
|
||||
if (!result.success) {
|
||||
result.error = output;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build git command with working directory
|
||||
*/
|
||||
std::string git_command(const std::string& repo_path, const std::string& cmd) {
|
||||
if (repo_path.empty()) {
|
||||
return "git " + cmd;
|
||||
}
|
||||
return "git -C \"" + repo_path + "\" " + cmd;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
bool is_git_available() {
|
||||
GitResult result = execute_command("git --version");
|
||||
return result.success;
|
||||
}
|
||||
|
||||
GitResult clone_repository(
|
||||
const std::string& url,
|
||||
const std::string& destination,
|
||||
const std::string& branch,
|
||||
int depth
|
||||
) {
|
||||
std::ostringstream cmd;
|
||||
cmd << "git clone";
|
||||
|
||||
if (!branch.empty()) {
|
||||
cmd << " --branch \"" << branch << "\"";
|
||||
}
|
||||
|
||||
if (depth > 0) {
|
||||
cmd << " --depth " << depth;
|
||||
}
|
||||
|
||||
cmd << " \"" << url << "\" \"" << destination << "\"";
|
||||
|
||||
return execute_command(cmd.str());
|
||||
}
|
||||
|
||||
GitResult create_branch(
|
||||
const std::string& repo_path,
|
||||
const std::string& branch_name,
|
||||
const std::string& base_branch
|
||||
) {
|
||||
std::ostringstream cmd;
|
||||
cmd << "checkout -b \"" << branch_name << "\"";
|
||||
|
||||
if (!base_branch.empty()) {
|
||||
cmd << " \"" << base_branch << "\"";
|
||||
}
|
||||
|
||||
return execute_command(git_command(repo_path, cmd.str()));
|
||||
}
|
||||
|
||||
GitResult checkout_branch(
|
||||
const std::string& repo_path,
|
||||
const std::string& branch_name
|
||||
) {
|
||||
std::string cmd = "checkout \"" + branch_name + "\"";
|
||||
return execute_command(git_command(repo_path, cmd));
|
||||
}
|
||||
|
||||
GitResult add_files(
|
||||
const std::string& repo_path,
|
||||
const std::vector<std::string>& files
|
||||
) {
|
||||
if (files.empty()) {
|
||||
GitResult result;
|
||||
result.success = true;
|
||||
result.output = "No files to add";
|
||||
result.exit_code = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::ostringstream cmd;
|
||||
cmd << "add";
|
||||
|
||||
for (const auto& file : files) {
|
||||
cmd << " \"" << file << "\"";
|
||||
}
|
||||
|
||||
return execute_command(git_command(repo_path, cmd.str()));
|
||||
}
|
||||
|
||||
GitResult commit(
|
||||
const std::string& repo_path,
|
||||
const std::string& message,
|
||||
const GitConfig& config
|
||||
) {
|
||||
// Set user config if provided
|
||||
if (!config.user_name.empty() && !config.user_email.empty()) {
|
||||
execute_command(git_command(repo_path,
|
||||
"config user.name \"" + config.user_name + "\""));
|
||||
execute_command(git_command(repo_path,
|
||||
"config user.email \"" + config.user_email + "\""));
|
||||
}
|
||||
|
||||
std::string cmd = "commit -m \"" + message + "\"";
|
||||
return execute_command(git_command(repo_path, cmd));
|
||||
}
|
||||
|
||||
GitResult push(
|
||||
const std::string& repo_path,
|
||||
const std::string& remote,
|
||||
const std::string& branch,
|
||||
bool force,
|
||||
const GitConfig& config
|
||||
) {
|
||||
std::ostringstream cmd;
|
||||
cmd << "push";
|
||||
|
||||
if (force) {
|
||||
cmd << " --force";
|
||||
}
|
||||
|
||||
// Set upstream if it's a new branch
|
||||
cmd << " --set-upstream \"" << remote << "\" \"" << branch << "\"";
|
||||
|
||||
std::string full_cmd = git_command(repo_path, cmd.str());
|
||||
|
||||
// If auth token is provided, inject it into the URL
|
||||
// This is a simplified approach; in production, use credential helpers
|
||||
if (!config.auth_token.empty()) {
|
||||
// Note: This assumes HTTPS URLs. For production, use git credential helpers
|
||||
// or SSH keys for better security
|
||||
std::cerr << "Note: Auth token provided. Consider using credential helpers for production." << std::endl;
|
||||
}
|
||||
|
||||
return execute_command(full_cmd);
|
||||
}
|
||||
|
||||
std::optional<std::string> get_current_branch(const std::string& repo_path) {
|
||||
GitResult result = execute_command(git_command(repo_path, "rev-parse --abbrev-ref HEAD"));
|
||||
|
||||
if (!result.success) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Trim whitespace
|
||||
std::string branch = result.output;
|
||||
branch.erase(branch.find_last_not_of(" \n\r\t") + 1);
|
||||
|
||||
return branch;
|
||||
}
|
||||
|
||||
bool branch_exists(const std::string& repo_path, const std::string& branch_name) {
|
||||
std::string cmd = "rev-parse --verify \"" + branch_name + "\"";
|
||||
GitResult result = execute_command(git_command(repo_path, cmd));
|
||||
return result.success;
|
||||
}
|
||||
|
||||
GitResult status(const std::string& repo_path) {
|
||||
return execute_command(git_command(repo_path, "status"));
|
||||
}
|
||||
|
||||
} // namespace git
|
||||
} // namespace wizardmerge
|
||||
205
backend/tests/test_git_cli.cpp
Normal file
205
backend/tests/test_git_cli.cpp
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* @file test_git_cli.cpp
|
||||
* @brief Unit tests for Git CLI wrapper functionality
|
||||
*/
|
||||
|
||||
#include "wizardmerge/git/git_cli.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
using namespace wizardmerge::git;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class GitCLITest : public ::testing::Test {
|
||||
protected:
|
||||
std::string test_dir;
|
||||
|
||||
void SetUp() override {
|
||||
// Create temporary test directory
|
||||
test_dir = "/tmp/wizardmerge_git_test_" + std::to_string(time(nullptr));
|
||||
fs::create_directories(test_dir);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Clean up test directory
|
||||
if (fs::exists(test_dir)) {
|
||||
fs::remove_all(test_dir);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to initialize a git repo
|
||||
void init_repo(const std::string& path) {
|
||||
system(("git init \"" + path + "\" 2>&1 > /dev/null").c_str());
|
||||
system(("git -C \"" + path + "\" config user.name \"Test User\"").c_str());
|
||||
system(("git -C \"" + path + "\" config user.email \"test@example.com\"").c_str());
|
||||
}
|
||||
|
||||
// Helper to create a file
|
||||
void create_file(const std::string& path, const std::string& content) {
|
||||
std::ofstream file(path);
|
||||
file << content;
|
||||
file.close();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Test Git availability check
|
||||
*/
|
||||
TEST_F(GitCLITest, GitAvailability) {
|
||||
// Git should be available in CI environment
|
||||
EXPECT_TRUE(is_git_available());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test branch existence check
|
||||
*/
|
||||
TEST_F(GitCLITest, BranchExists) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create initial commit (required for branch operations)
|
||||
create_file(repo_path + "/test.txt", "initial content");
|
||||
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str());
|
||||
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str());
|
||||
|
||||
// Default branch should exist (main or master)
|
||||
auto current_branch = get_current_branch(repo_path);
|
||||
ASSERT_TRUE(current_branch.has_value());
|
||||
EXPECT_TRUE(branch_exists(repo_path, current_branch.value()));
|
||||
|
||||
// Non-existent branch should not exist
|
||||
EXPECT_FALSE(branch_exists(repo_path, "nonexistent-branch"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting current branch
|
||||
*/
|
||||
TEST_F(GitCLITest, GetCurrentBranch) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create initial commit
|
||||
create_file(repo_path + "/test.txt", "initial content");
|
||||
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str());
|
||||
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str());
|
||||
|
||||
auto branch = get_current_branch(repo_path);
|
||||
ASSERT_TRUE(branch.has_value());
|
||||
// Should be either "main" or "master" depending on Git version
|
||||
EXPECT_TRUE(branch.value() == "main" || branch.value() == "master");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating a new branch
|
||||
*/
|
||||
TEST_F(GitCLITest, CreateBranch) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create initial commit
|
||||
create_file(repo_path + "/test.txt", "initial content");
|
||||
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str());
|
||||
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str());
|
||||
|
||||
// Create new branch
|
||||
GitResult result = create_branch(repo_path, "test-branch");
|
||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||
|
||||
// Verify we're on the new branch
|
||||
auto current_branch = get_current_branch(repo_path);
|
||||
ASSERT_TRUE(current_branch.has_value());
|
||||
EXPECT_EQ(current_branch.value(), "test-branch");
|
||||
|
||||
// Verify branch exists
|
||||
EXPECT_TRUE(branch_exists(repo_path, "test-branch"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding files
|
||||
*/
|
||||
TEST_F(GitCLITest, AddFiles) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create test files
|
||||
create_file(repo_path + "/file1.txt", "content1");
|
||||
create_file(repo_path + "/file2.txt", "content2");
|
||||
|
||||
// Add files
|
||||
GitResult result = add_files(repo_path, {"file1.txt", "file2.txt"});
|
||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test committing changes
|
||||
*/
|
||||
TEST_F(GitCLITest, Commit) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create and add a file
|
||||
create_file(repo_path + "/test.txt", "content");
|
||||
add_files(repo_path, {"test.txt"});
|
||||
|
||||
// Commit
|
||||
GitConfig config;
|
||||
config.user_name = "Test User";
|
||||
config.user_email = "test@example.com";
|
||||
|
||||
GitResult result = commit(repo_path, "Test commit", config);
|
||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test repository status
|
||||
*/
|
||||
TEST_F(GitCLITest, Status) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
GitResult result = status(repo_path);
|
||||
EXPECT_TRUE(result.success);
|
||||
EXPECT_FALSE(result.output.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test checkout branch
|
||||
*/
|
||||
TEST_F(GitCLITest, CheckoutBranch) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create initial commit
|
||||
create_file(repo_path + "/test.txt", "initial content");
|
||||
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str());
|
||||
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str());
|
||||
|
||||
// Create and switch to new branch
|
||||
create_branch(repo_path, "test-branch");
|
||||
|
||||
// Get original branch
|
||||
auto original_branch = get_current_branch(repo_path);
|
||||
system(("git -C \"" + repo_path + "\" checkout " + original_branch.value() + " 2>&1 > /dev/null").c_str());
|
||||
|
||||
// Checkout the test branch
|
||||
GitResult result = checkout_branch(repo_path, "test-branch");
|
||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||
|
||||
// Verify we're on test-branch
|
||||
auto current_branch = get_current_branch(repo_path);
|
||||
ASSERT_TRUE(current_branch.has_value());
|
||||
EXPECT_EQ(current_branch.value(), "test-branch");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test empty file list
|
||||
*/
|
||||
TEST_F(GitCLITest, AddEmptyFileList) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Add empty file list should succeed without error
|
||||
GitResult result = add_files(repo_path, {});
|
||||
EXPECT_TRUE(result.success);
|
||||
}
|
||||
Reference in New Issue
Block a user