From b2d25c6f624443cc8f8221fc28583dbfa7b710fc Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 04:19:21 +0000 Subject: [PATCH] Replace JsonCpp parsing with lightweight helpers --- backend/src/git/git_platform_client.cpp | 296 ++++++++++++++++-------- 1 file changed, 197 insertions(+), 99 deletions(-) diff --git a/backend/src/git/git_platform_client.cpp b/backend/src/git/git_platform_client.cpp index 4a9aa76..1dc170d 100644 --- a/backend/src/git/git_platform_client.cpp +++ b/backend/src/git/git_platform_client.cpp @@ -8,14 +8,135 @@ #include #include #include +#include +#include #include -#include namespace wizardmerge { namespace git { namespace { +std::optional 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 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 extract_objects_from_array(const std::string& array) { + std::vector objects; + + int depth = 0; + std::optional 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 */ @@ -216,41 +337,32 @@ std::optional fetch_pull_request( return std::nullopt; } - // Parse JSON response - Json::Value root; - Json::CharReaderBuilder reader; - std::string errs; - std::istringstream s(response); - - if (!Json::parseFromStream(reader, s, &root, &errs)) { - std::cerr << "Failed to parse PR/MR JSON: " << errs << std::endl; - return std::nullopt; - } + pr.title = extract_string_field(response, "title"); + pr.state = extract_string_field(response, "state"); - pr.title = root.get("title", "").asString(); - pr.state = root.get("state", "").asString(); - if (platform == GitPlatform::GitHub) { - if (root.isMember("base") && root["base"].isObject()) { - pr.base_ref = root["base"].get("ref", "").asString(); - pr.base_sha = root["base"].get("sha", "").asString(); + if (auto base_object = extract_object_segment(response, "base")) { + pr.base_ref = extract_string_field(*base_object, "ref"); + pr.base_sha = extract_string_field(*base_object, "sha"); } - - if (root.isMember("head") && root["head"].isObject()) { - pr.head_ref = root["head"].get("ref", "").asString(); - pr.head_sha = root["head"].get("sha", "").asString(); + + if (auto head_object = extract_object_segment(response, "head")) { + pr.head_ref = extract_string_field(*head_object, "ref"); + pr.head_sha = extract_string_field(*head_object, "sha"); } - - pr.mergeable = root.get("mergeable", false).asBool(); - pr.mergeable_state = root.get("mergeable_state", "unknown").asString(); + + pr.mergeable = extract_bool_field(response, "mergeable", false); + pr.mergeable_state = extract_string_field(response, "mergeable_state", "unknown"); } else if (platform == GitPlatform::GitLab) { - pr.base_ref = root.get("target_branch", "").asString(); - pr.head_ref = root.get("source_branch", "").asString(); - pr.base_sha = root.get("diff_refs", Json::Value::null).get("base_sha", "").asString(); - pr.head_sha = root.get("diff_refs", Json::Value::null).get("head_sha", "").asString(); - - // GitLab uses different merge status - std::string merge_status = root.get("merge_status", "").asString(); + pr.base_ref = extract_string_field(response, "target_branch"); + pr.head_ref = extract_string_field(response, "source_branch"); + + if (auto diff_refs = extract_object_segment(response, "diff_refs")) { + 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_state = merge_status; } @@ -263,58 +375,56 @@ std::optional fetch_pull_request( return std::nullopt; } - Json::Value files_root; - std::istringstream files_stream(files_response); - - if (!Json::parseFromStream(reader, files_stream, &files_root, &errs)) { - std::cerr << "Failed to parse files JSON: " << errs << std::endl; - return std::nullopt; + // Process files based on platform + if (platform == GitPlatform::GitHub) { + for (const auto& file_object : extract_objects_from_array(files_response)) { + PRFile pr_file; + pr_file.filename = extract_string_field(file_object, "filename"); + pr_file.status = extract_string_field(file_object, "status"); + pr_file.additions = extract_int_field(file_object, "additions"); + pr_file.deletions = extract_int_field(file_object, "deletions"); + pr_file.changes = extract_int_field(file_object, "changes"); + + pr.files.push_back(pr_file); + } + } else if (platform == GitPlatform::GitLab) { + if (auto changes_array = extract_array_segment(files_response, "changes")) { + for (const auto& file_object : extract_objects_from_array(*changes_array)) { + PRFile pr_file; + 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"); + } + + bool new_file = extract_bool_field(file_object, "new_file", false); + bool deleted_file = extract_bool_field(file_object, "deleted_file", false); + bool renamed_file = extract_bool_field(file_object, "renamed_file", false); + + if (new_file) { + pr_file.status = "added"; + } else if (deleted_file) { + pr_file.status = "removed"; + } else if (renamed_file) { + pr_file.status = "renamed"; + } else { + pr_file.status = "modified"; + } + + pr_file.additions = 0; + pr_file.deletions = 0; + pr_file.changes = 0; + + pr.files.push_back(pr_file); + } + } } - // Process files based on platform - if (platform == GitPlatform::GitHub && files_root.isArray()) { - // GitHub format: array of file objects - for (const auto& file : files_root) { - PRFile pr_file; - pr_file.filename = file.get("filename", "").asString(); - pr_file.status = file.get("status", "").asString(); - pr_file.additions = file.get("additions", 0).asInt(); - pr_file.deletions = file.get("deletions", 0).asInt(); - pr_file.changes = file.get("changes", 0).asInt(); - - pr.files.push_back(pr_file); - } - } else if (platform == GitPlatform::GitLab && files_root.isMember("changes")) { - // GitLab format: object with "changes" array - const Json::Value& changes = files_root["changes"]; - if (changes.isArray()) { - for (const auto& file : changes) { - PRFile pr_file; - pr_file.filename = file.get("new_path", file.get("old_path", "").asString()).asString(); - - // Determine status from new_file, deleted_file, renamed_file flags - bool new_file = file.get("new_file", false).asBool(); - bool deleted_file = file.get("deleted_file", false).asBool(); - bool renamed_file = file.get("renamed_file", false).asBool(); - - if (new_file) { - pr_file.status = "added"; - } else if (deleted_file) { - pr_file.status = "removed"; - } else if (renamed_file) { - pr_file.status = "renamed"; - } else { - pr_file.status = "modified"; - } - - // GitLab doesn't provide addition/deletion counts in the changes endpoint - pr_file.additions = 0; - pr_file.deletions = 0; - pr_file.changes = 0; - - pr.files.push_back(pr_file); - } - } + 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; } } @@ -366,43 +476,31 @@ std::optional> fetch_file_content( // Handle response based on platform if (platform == GitPlatform::GitHub) { // GitHub returns JSON with base64-encoded content - Json::Value root; - Json::CharReaderBuilder reader; - std::string errs; - std::istringstream s(response); - - if (!Json::parseFromStream(reader, s, &root, &errs)) { - std::cerr << "Failed to parse content JSON: " << errs << std::endl; - return std::nullopt; - } + std::string encoding = extract_string_field(response, "encoding"); + std::string encoded_content = extract_string_field(response, "content"); - // GitHub API returns content as base64 encoded - if (!root.isMember("content") || !root.isMember("encoding")) { + if (encoding.empty() || encoded_content.empty()) { std::cerr << "Invalid response format for file content" << std::endl; return std::nullopt; } - std::string encoding = root["encoding"].asString(); if (encoding != "base64") { std::cerr << "Unsupported encoding: " << encoding << std::endl; return std::nullopt; } - // Decode base64 content - std::string encoded_content = root["content"].asString(); - // Remove newlines from base64 string encoded_content.erase(std::remove(encoded_content.begin(), encoded_content.end(), '\n'), encoded_content.end()); encoded_content.erase(std::remove(encoded_content.begin(), encoded_content.end(), '\r'), encoded_content.end()); - + // Decode base64 std::string decoded_content = base64_decode(encoded_content); - + if (decoded_content.empty()) { std::cerr << "Failed to decode base64 content" << std::endl; return std::nullopt; } - + // Split content into lines return split_lines(decoded_content); } else if (platform == GitPlatform::GitLab) {