mirror of
https://github.com/johndoe6345789/WizardMerge.git
synced 2026-04-24 13:44:55 +00:00
Add PR URL support with GitHub API integration
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
@@ -9,10 +9,12 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
# Find dependencies via Conan
|
||||
find_package(Drogon CONFIG QUIET)
|
||||
find_package(GTest QUIET)
|
||||
find_package(CURL QUIET)
|
||||
|
||||
# Library sources
|
||||
add_library(wizardmerge
|
||||
src/merge/three_way_merge.cpp
|
||||
src/git/github_client.cpp
|
||||
)
|
||||
|
||||
target_include_directories(wizardmerge
|
||||
@@ -21,11 +23,17 @@ target_include_directories(wizardmerge
|
||||
$<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)
|
||||
add_executable(wizardmerge-cli
|
||||
src/main.cpp
|
||||
src/controllers/MergeController.cc
|
||||
src/controllers/PRController.cc
|
||||
)
|
||||
|
||||
target_link_libraries(wizardmerge-cli PRIVATE wizardmerge Drogon::Drogon)
|
||||
@@ -44,6 +52,7 @@ if(GTest_FOUND)
|
||||
enable_testing()
|
||||
add_executable(wizardmerge-tests
|
||||
tests/test_three_way_merge.cpp
|
||||
tests/test_github_client.cpp
|
||||
)
|
||||
target_link_libraries(wizardmerge-tests PRIVATE wizardmerge GTest::gtest_main)
|
||||
|
||||
|
||||
101
backend/include/wizardmerge/git/github_client.h
Normal file
101
backend/include/wizardmerge/git/github_client.h
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @file github_client.h
|
||||
* @brief GitHub API client for fetching pull request information
|
||||
*/
|
||||
|
||||
#ifndef WIZARDMERGE_GIT_GITHUB_CLIENT_H
|
||||
#define WIZARDMERGE_GIT_GITHUB_CLIENT_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace git {
|
||||
|
||||
/**
|
||||
* @brief Information about a file in a pull request
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Pull request information from GitHub
|
||||
*/
|
||||
struct PullRequest {
|
||||
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 GitHub pull request URL
|
||||
*
|
||||
* Extracts owner, repo, and PR number from URLs like:
|
||||
* - https://github.com/owner/repo/pull/123
|
||||
* - github.com/owner/repo/pull/123
|
||||
*
|
||||
* @param url The pull request URL
|
||||
* @param owner Output repository owner
|
||||
* @param repo Output repository name
|
||||
* @param pr_number Output PR number
|
||||
* @return true if successfully parsed, false otherwise
|
||||
*/
|
||||
bool parse_pr_url(const std::string& url, std::string& owner,
|
||||
std::string& repo, int& pr_number);
|
||||
|
||||
/**
|
||||
* @brief Fetch pull request information from GitHub API
|
||||
*
|
||||
* @param owner Repository owner
|
||||
* @param repo Repository name
|
||||
* @param pr_number Pull request number
|
||||
* @param token Optional GitHub API token for authentication
|
||||
* @return Pull request information, or empty optional on error
|
||||
*/
|
||||
std::optional<PullRequest> fetch_pull_request(
|
||||
const std::string& owner,
|
||||
const std::string& repo,
|
||||
int pr_number,
|
||||
const std::string& token = ""
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Fetch file content from GitHub at a specific commit
|
||||
*
|
||||
* @param owner Repository owner
|
||||
* @param repo Repository name
|
||||
* @param sha Commit SHA
|
||||
* @param path File path
|
||||
* @param token Optional GitHub API token
|
||||
* @return File content as vector of lines, or empty optional on error
|
||||
*/
|
||||
std::optional<std::vector<std::string>> fetch_file_content(
|
||||
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_GITHUB_CLIENT_H
|
||||
188
backend/src/controllers/PRController.cc
Normal file
188
backend/src/controllers/PRController.cc
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* @file PRController.cc
|
||||
* @brief Implementation of HTTP controller for pull request operations
|
||||
*/
|
||||
|
||||
#include "PRController.h"
|
||||
#include "wizardmerge/git/github_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 github_token = json.get("github_token", "").asString();
|
||||
bool create_branch = json.get("create_branch", false).asBool();
|
||||
std::string branch_name = json.get("branch_name", "").asString();
|
||||
|
||||
// Parse PR URL
|
||||
std::string owner, repo;
|
||||
int pr_number;
|
||||
|
||||
if (!parse_pr_url(pr_url, owner, repo, pr_number)) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid pull request URL format";
|
||||
error["pr_url"] = pr_url;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch pull request information
|
||||
auto pr_opt = fetch_pull_request(owner, repo, pr_number, github_token);
|
||||
|
||||
if (!pr_opt) {
|
||||
Json::Value error;
|
||||
error["error"] = "Failed to fetch pull request information";
|
||||
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(owner, repo, pr.base_sha, file.filename, github_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(owner, repo, pr.head_sha, file.filename, github_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
|
||||
// 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
|
||||
|
||||
// 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["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
|
||||
257
backend/src/git/github_client.cpp
Normal file
257
backend/src/git/github_client.cpp
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* @file github_client.cpp
|
||||
* @brief Implementation of GitHub API client
|
||||
*/
|
||||
|
||||
#include "wizardmerge/git/github_client.h"
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <curl/curl.h>
|
||||
#include <json/json.h>
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace git {
|
||||
|
||||
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);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Perform HTTP GET request using libcurl
|
||||
*/
|
||||
bool http_get(const std::string& url, const std::string& token, std::string& response) {
|
||||
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
|
||||
struct curl_slist* headers = nullptr;
|
||||
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());
|
||||
}
|
||||
|
||||
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, std::string& owner,
|
||||
std::string& repo, int& pr_number) {
|
||||
// Match patterns like:
|
||||
// https://github.com/owner/repo/pull/123
|
||||
// github.com/owner/repo/pull/123
|
||||
// owner/repo/pull/123
|
||||
|
||||
std::regex pr_regex(R"((?:https?://)?(?:www\.)?github\.com/([^/]+)/([^/]+)/pull/(\d+))");
|
||||
std::smatch matches;
|
||||
|
||||
if (std::regex_search(url, matches, pr_regex)) {
|
||||
if (matches.size() == 4) {
|
||||
owner = matches[1].str();
|
||||
repo = matches[2].str();
|
||||
pr_number = std::stoi(matches[3].str());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<PullRequest> fetch_pull_request(
|
||||
const std::string& owner,
|
||||
const std::string& repo,
|
||||
int pr_number,
|
||||
const std::string& token
|
||||
) {
|
||||
// Fetch PR info
|
||||
std::string pr_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + std::to_string(pr_number);
|
||||
std::string response;
|
||||
|
||||
if (!http_get(pr_url, token, response)) {
|
||||
std::cerr << "Failed to fetch pull 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 JSON: " << errs << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
PullRequest pr;
|
||||
pr.number = pr_number;
|
||||
pr.title = root.get("title", "").asString();
|
||||
pr.state = root.get("state", "").asString();
|
||||
pr.repo_owner = owner;
|
||||
pr.repo_name = repo;
|
||||
|
||||
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();
|
||||
|
||||
// Fetch PR files
|
||||
std::string files_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + std::to_string(pr_number) + "/files";
|
||||
std::string files_response;
|
||||
|
||||
if (!http_get(files_url, token, files_response)) {
|
||||
std::cerr << "Failed to fetch pull 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;
|
||||
}
|
||||
|
||||
if (files_root.isArray()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return pr;
|
||||
}
|
||||
|
||||
std::optional<std::vector<std::string>> fetch_file_content(
|
||||
const std::string& owner,
|
||||
const std::string& repo,
|
||||
const std::string& sha,
|
||||
const std::string& path,
|
||||
const std::string& token
|
||||
) {
|
||||
// Use GitHub API to get file content at specific commit
|
||||
std::string url = "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" + path + "?ref=" + sha;
|
||||
std::string response;
|
||||
|
||||
if (!http_get(url, token, response)) {
|
||||
std::cerr << "Failed to fetch file content for " << path << " at " << sha << 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 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());
|
||||
|
||||
// Simple base64 decode (using curl's built-in decoder)
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
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);
|
||||
std::cerr << "Failed to decode base64 content" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string decoded_content(reinterpret_cast<char*>(decoded), outlen);
|
||||
curl_free(decoded);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
// Split content into lines
|
||||
return split_lines(decoded_content);
|
||||
}
|
||||
|
||||
} // namespace git
|
||||
} // namespace wizardmerge
|
||||
69
backend/tests/test_github_client.cpp
Normal file
69
backend/tests/test_github_client.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file test_github_client.cpp
|
||||
* @brief Unit tests for GitHub client functionality
|
||||
*/
|
||||
|
||||
#include "wizardmerge/git/github_client.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace wizardmerge::git;
|
||||
|
||||
/**
|
||||
* Test PR URL parsing with various formats
|
||||
*/
|
||||
TEST(GitHubClientTest, ParsePRUrl_ValidUrls) {
|
||||
std::string owner, repo;
|
||||
int pr_number;
|
||||
|
||||
// Test full HTTPS URL
|
||||
ASSERT_TRUE(parse_pr_url("https://github.com/owner/repo/pull/123", owner, repo, pr_number));
|
||||
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", owner, repo, pr_number));
|
||||
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", owner, repo, pr_number));
|
||||
EXPECT_EQ(owner, "testuser");
|
||||
EXPECT_EQ(repo, "testrepo");
|
||||
EXPECT_EQ(pr_number, 789);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PR URL parsing with invalid formats
|
||||
*/
|
||||
TEST(GitHubClientTest, ParsePRUrl_InvalidUrls) {
|
||||
std::string owner, repo;
|
||||
int pr_number;
|
||||
|
||||
// Missing PR number
|
||||
EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo/pull/", owner, repo, pr_number));
|
||||
|
||||
// Invalid format
|
||||
EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo", owner, repo, pr_number));
|
||||
|
||||
// Not a GitHub URL
|
||||
EXPECT_FALSE(parse_pr_url("https://gitlab.com/owner/repo/pull/123", owner, repo, pr_number));
|
||||
|
||||
// Empty string
|
||||
EXPECT_FALSE(parse_pr_url("", owner, repo, pr_number));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PR URL with special characters in owner/repo names
|
||||
*/
|
||||
TEST(GitHubClientTest, ParsePRUrl_SpecialCharacters) {
|
||||
std::string owner, repo;
|
||||
int pr_number;
|
||||
|
||||
// Underscores and hyphens
|
||||
ASSERT_TRUE(parse_pr_url("https://github.com/my-owner_123/my-repo_456/pull/999", owner, repo, pr_number));
|
||||
EXPECT_EQ(owner, "my-owner_123");
|
||||
EXPECT_EQ(repo, "my-repo_456");
|
||||
EXPECT_EQ(pr_number, 999);
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
#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
|
||||
@@ -12,6 +15,7 @@ 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";
|
||||
@@ -28,11 +32,18 @@ void printUsage(const char* programName) {
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -55,12 +66,19 @@ int main(int argc, char* argv[]) {
|
||||
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) {
|
||||
@@ -85,8 +103,31 @@ int main(int argc, char* argv[]) {
|
||||
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];
|
||||
@@ -231,6 +272,117 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user