1 Commits

Author SHA1 Message Date
b2d25c6f62 Replace JsonCpp parsing with lightweight helpers 2025-12-27 04:19:21 +00:00

View File

@@ -8,14 +8,135 @@
#include <sstream> #include <sstream>
#include <iostream> #include <iostream>
#include <algorithm> #include <algorithm>
#include <optional>
#include <vector>
#include <curl/curl.h> #include <curl/curl.h>
#include <json/json.h>
namespace wizardmerge { namespace wizardmerge {
namespace git { namespace git {
namespace { namespace {
std::optional<std::string> extract_object_segment(const std::string& json,
const std::string& key) {
auto key_pos = json.find("\"" + key + "\"");
if (key_pos == std::string::npos) {
return std::nullopt;
}
auto brace_pos = json.find('{', key_pos);
if (brace_pos == std::string::npos) {
return std::nullopt;
}
int depth = 0;
for (size_t i = brace_pos; i < json.size(); ++i) {
if (json[i] == '{') {
depth++;
} else if (json[i] == '}') {
depth--;
if (depth == 0) {
return json.substr(brace_pos, i - brace_pos + 1);
}
}
}
return std::nullopt;
}
std::optional<std::string> extract_array_segment(const std::string& json,
const std::string& key) {
auto key_pos = json.find("\"" + key + "\"");
if (key_pos == std::string::npos) {
return std::nullopt;
}
auto bracket_pos = json.find('[', key_pos);
if (bracket_pos == std::string::npos) {
return std::nullopt;
}
int depth = 0;
for (size_t i = bracket_pos; i < json.size(); ++i) {
if (json[i] == '[') {
depth++;
} else if (json[i] == ']') {
depth--;
if (depth == 0) {
return json.substr(bracket_pos, i - bracket_pos + 1);
}
}
}
return std::nullopt;
}
std::vector<std::string> extract_objects_from_array(const std::string& array) {
std::vector<std::string> objects;
int depth = 0;
std::optional<size_t> start;
for (size_t i = 0; i < array.size(); ++i) {
if (array[i] == '{') {
if (!start) {
start = i;
}
depth++;
} else if (array[i] == '}') {
depth--;
if (depth == 0 && start) {
objects.push_back(array.substr(*start, i - *start + 1));
start.reset();
}
}
}
return objects;
}
std::string extract_string_field(const std::string& json,
const std::string& key,
const std::string& default_value = "") {
std::regex pattern("\\\\\"" + key + "\\\\\"\\s*:\\s*\\\\\"([^\\\\\"]*)\\\\\"");
std::smatch match;
if (std::regex_search(json, match, pattern) && match.size() >= 2) {
return match[1];
}
return default_value;
}
bool extract_bool_field(const std::string& json,
const std::string& key,
bool default_value = false) {
std::regex pattern("\\\\\"" + key + "\\\\\"\\s*:\\s*(true|false)");
std::smatch match;
if (std::regex_search(json, match, pattern) && match.size() >= 2) {
return match[1] == "true";
}
return default_value;
}
int extract_int_field(const std::string& json,
const std::string& key,
int default_value = 0) {
std::regex pattern("\\\\\"" + key + "\\\\\"\\s*:\\s*(-?\\d+)");
std::smatch match;
if (std::regex_search(json, match, pattern) && match.size() >= 2) {
try {
return std::stoi(match[1]);
} catch (const std::exception&) {
}
}
return default_value;
}
/** /**
* @brief Simple base64 decoder * @brief Simple base64 decoder
*/ */
@@ -216,41 +337,32 @@ std::optional<PullRequest> fetch_pull_request(
return std::nullopt; return std::nullopt;
} }
// Parse JSON response pr.title = extract_string_field(response, "title");
Json::Value root; pr.state = extract_string_field(response, "state");
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 (platform == GitPlatform::GitHub) {
if (root.isMember("base") && root["base"].isObject()) { if (auto base_object = extract_object_segment(response, "base")) {
pr.base_ref = root["base"].get("ref", "").asString(); pr.base_ref = extract_string_field(*base_object, "ref");
pr.base_sha = root["base"].get("sha", "").asString(); pr.base_sha = extract_string_field(*base_object, "sha");
} }
if (root.isMember("head") && root["head"].isObject()) { if (auto head_object = extract_object_segment(response, "head")) {
pr.head_ref = root["head"].get("ref", "").asString(); pr.head_ref = extract_string_field(*head_object, "ref");
pr.head_sha = root["head"].get("sha", "").asString(); pr.head_sha = extract_string_field(*head_object, "sha");
} }
pr.mergeable = root.get("mergeable", false).asBool(); pr.mergeable = extract_bool_field(response, "mergeable", false);
pr.mergeable_state = root.get("mergeable_state", "unknown").asString(); pr.mergeable_state = extract_string_field(response, "mergeable_state", "unknown");
} else if (platform == GitPlatform::GitLab) { } else if (platform == GitPlatform::GitLab) {
pr.base_ref = root.get("target_branch", "").asString(); pr.base_ref = extract_string_field(response, "target_branch");
pr.head_ref = root.get("source_branch", "").asString(); pr.head_ref = extract_string_field(response, "source_branch");
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 if (auto diff_refs = extract_object_segment(response, "diff_refs")) {
std::string merge_status = root.get("merge_status", "").asString(); pr.base_sha = extract_string_field(*diff_refs, "base_sha");
pr.head_sha = extract_string_field(*diff_refs, "head_sha");
}
std::string merge_status = extract_string_field(response, "merge_status");
pr.mergeable = (merge_status == "can_be_merged"); pr.mergeable = (merge_status == "can_be_merged");
pr.mergeable_state = merge_status; pr.mergeable_state = merge_status;
} }
@@ -263,39 +375,32 @@ std::optional<PullRequest> fetch_pull_request(
return std::nullopt; 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 // Process files based on platform
if (platform == GitPlatform::GitHub && files_root.isArray()) { if (platform == GitPlatform::GitHub) {
// GitHub format: array of file objects for (const auto& file_object : extract_objects_from_array(files_response)) {
for (const auto& file : files_root) {
PRFile pr_file; PRFile pr_file;
pr_file.filename = file.get("filename", "").asString(); pr_file.filename = extract_string_field(file_object, "filename");
pr_file.status = file.get("status", "").asString(); pr_file.status = extract_string_field(file_object, "status");
pr_file.additions = file.get("additions", 0).asInt(); pr_file.additions = extract_int_field(file_object, "additions");
pr_file.deletions = file.get("deletions", 0).asInt(); pr_file.deletions = extract_int_field(file_object, "deletions");
pr_file.changes = file.get("changes", 0).asInt(); pr_file.changes = extract_int_field(file_object, "changes");
pr.files.push_back(pr_file); pr.files.push_back(pr_file);
} }
} else if (platform == GitPlatform::GitLab && files_root.isMember("changes")) { } else if (platform == GitPlatform::GitLab) {
// GitLab format: object with "changes" array if (auto changes_array = extract_array_segment(files_response, "changes")) {
const Json::Value& changes = files_root["changes"]; for (const auto& file_object : extract_objects_from_array(*changes_array)) {
if (changes.isArray()) {
for (const auto& file : changes) {
PRFile pr_file; PRFile pr_file;
pr_file.filename = file.get("new_path", file.get("old_path", "").asString()).asString(); std::string new_path = extract_string_field(file_object, "new_path");
if (!new_path.empty()) {
pr_file.filename = new_path;
} else {
pr_file.filename = extract_string_field(file_object, "old_path");
}
// Determine status from new_file, deleted_file, renamed_file flags bool new_file = extract_bool_field(file_object, "new_file", false);
bool new_file = file.get("new_file", false).asBool(); bool deleted_file = extract_bool_field(file_object, "deleted_file", false);
bool deleted_file = file.get("deleted_file", false).asBool(); bool renamed_file = extract_bool_field(file_object, "renamed_file", false);
bool renamed_file = file.get("renamed_file", false).asBool();
if (new_file) { if (new_file) {
pr_file.status = "added"; pr_file.status = "added";
@@ -307,7 +412,6 @@ std::optional<PullRequest> fetch_pull_request(
pr_file.status = "modified"; pr_file.status = "modified";
} }
// GitLab doesn't provide addition/deletion counts in the changes endpoint
pr_file.additions = 0; pr_file.additions = 0;
pr_file.deletions = 0; pr_file.deletions = 0;
pr_file.changes = 0; pr_file.changes = 0;
@@ -316,6 +420,12 @@ std::optional<PullRequest> fetch_pull_request(
} }
} }
} }
if (platform == GitPlatform::GitHub || platform == GitPlatform::GitLab) {
// If parsing failed and we have no files, signal an error to callers.
if (pr.files.empty() && !files_response.empty()) {
std::cerr << "No files parsed from API response" << std::endl;
}
} }
return pr; return pr;
@@ -366,31 +476,19 @@ std::optional<std::vector<std::string>> fetch_file_content(
// Handle response based on platform // Handle response based on platform
if (platform == GitPlatform::GitHub) { if (platform == GitPlatform::GitHub) {
// GitHub returns JSON with base64-encoded content // GitHub returns JSON with base64-encoded content
Json::Value root; std::string encoding = extract_string_field(response, "encoding");
Json::CharReaderBuilder reader; std::string encoded_content = extract_string_field(response, "content");
std::string errs;
std::istringstream s(response);
if (!Json::parseFromStream(reader, s, &root, &errs)) { if (encoding.empty() || encoded_content.empty()) {
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; std::cerr << "Invalid response format for file content" << std::endl;
return std::nullopt; return std::nullopt;
} }
std::string encoding = root["encoding"].asString();
if (encoding != "base64") { if (encoding != "base64") {
std::cerr << "Unsupported encoding: " << encoding << std::endl; std::cerr << "Unsupported encoding: " << encoding << std::endl;
return std::nullopt; return std::nullopt;
} }
// Decode base64 content
std::string encoded_content = root["content"].asString();
// Remove newlines from base64 string // 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(), '\n'), encoded_content.end());
encoded_content.erase(std::remove(encoded_content.begin(), encoded_content.end(), '\r'), encoded_content.end()); encoded_content.erase(std::remove(encoded_content.begin(), encoded_content.end(), '\r'), encoded_content.end());