From c5a7f89b3f5e98bdd0c542f013cf9f0a5f24e54b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 02:05:15 +0000 Subject: [PATCH] Fix code review issues: base64 decoding, includes, and struct fields Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- backend/examples/pr_resolve_example.py | 193 ++++++++++++++++++ .../include/wizardmerge/git/github_client.h | 2 - backend/src/controllers/PRController.cc | 9 +- backend/src/git/github_client.cpp | 48 +++-- 4 files changed, 232 insertions(+), 20 deletions(-) create mode 100755 backend/examples/pr_resolve_example.py diff --git a/backend/examples/pr_resolve_example.py b/backend/examples/pr_resolve_example.py new file mode 100755 index 0000000..a4377d8 --- /dev/null +++ b/backend/examples/pr_resolve_example.py @@ -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() diff --git a/backend/include/wizardmerge/git/github_client.h b/backend/include/wizardmerge/git/github_client.h index 72f24c1..834b53a 100644 --- a/backend/include/wizardmerge/git/github_client.h +++ b/backend/include/wizardmerge/git/github_client.h @@ -20,8 +20,6 @@ namespace git { struct PRFile { std::string filename; std::string status; // "added", "modified", "removed", "renamed" - std::string base_content; - std::string head_content; int additions; int deletions; int changes; diff --git a/backend/src/controllers/PRController.cc b/backend/src/controllers/PRController.cc index 45ab895..07f85f2 100644 --- a/backend/src/controllers/PRController.cc +++ b/backend/src/controllers/PRController.cc @@ -125,8 +125,13 @@ void PRController::resolvePR( std::vector head_content = head_opt.value(); // For added files or when there might be a conflict with existing file - // We use the current head as "ours" and try to merge with base - // This is simplified - in reality, we'd need to detect actual merge conflicts + // 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); diff --git a/backend/src/git/github_client.cpp b/backend/src/git/github_client.cpp index f52b081..3757048 100644 --- a/backend/src/git/github_client.cpp +++ b/backend/src/git/github_client.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -15,6 +16,34 @@ 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 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; +} + +namespace { + // 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); @@ -228,27 +257,14 @@ std::optional> fetch_file_content( 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()); - // Simple base64 decode (using curl's built-in decoder) - CURL* curl = curl_easy_init(); - if (!curl) { - return std::nullopt; - } + // Decode base64 + std::string decoded_content = base64_decode(encoded_content); - int outlen; - unsigned char* decoded = curl_easy_unescape(curl, encoded_content.c_str(), encoded_content.length(), &outlen); - - if (!decoded) { - // Fallback: try manual base64 decode - // For now, return empty as we need proper base64 decoder - curl_easy_cleanup(curl); + if (decoded_content.empty()) { std::cerr << "Failed to decode base64 content" << std::endl; return std::nullopt; } - std::string decoded_content(reinterpret_cast(decoded), outlen); - curl_free(decoded); - curl_easy_cleanup(curl); - // Split content into lines return split_lines(decoded_content); }