diff --git a/backend/examples/typescript_example.cpp b/backend/examples/typescript_example.cpp index 7798068..99510e2 100644 --- a/backend/examples/typescript_example.cpp +++ b/backend/examples/typescript_example.cpp @@ -6,186 +6,165 @@ #include "wizardmerge/analysis/context_analyzer.h" #include "wizardmerge/analysis/risk_analyzer.h" #include -#include #include +#include using namespace wizardmerge::analysis; void print_separator() { - std::cout << "\n" << std::string(60, '=') << "\n" << std::endl; + std::cout << "\n" << std::string(60, '=') << "\n" << std::endl; } int main() { - std::cout << "WizardMerge TypeScript Support Demo" << std::endl; - print_separator(); - - // Example 1: TypeScript Function Detection - std::cout << "Example 1: TypeScript Function Detection" << std::endl; - std::cout << std::string(40, '-') << std::endl; - - std::vector ts_functions = { - "export async function fetchUser(id: number): Promise {", - " const response = await api.get(`/users/${id}`);", - " return response.data;", - "}" - }; - - std::string func_name = extract_function_name(ts_functions, 1); - std::cout << "Detected function: " << func_name << std::endl; - print_separator(); - - // Example 2: TypeScript Interface Detection - std::cout << "Example 2: TypeScript Interface Detection" << std::endl; - std::cout << std::string(40, '-') << std::endl; - - std::vector ts_interface = { - "export interface User {", - " id: number;", - " name: string;", - " email: string;", - "}" - }; - - std::string type_name = extract_class_name(ts_interface, 2); - std::cout << "Detected type: " << type_name << std::endl; - print_separator(); - - // Example 3: TypeScript Import Extraction - std::cout << "Example 3: TypeScript Import Extraction" << std::endl; - std::cout << std::string(40, '-') << std::endl; - - std::vector ts_imports = { - "import { Component, useState } from 'react';", - "import type { User } from './types';", - "import * as utils from './utils';", - "", - "export const MyComponent = () => {" - }; - - auto imports = extract_imports(ts_imports); - std::cout << "Detected " << imports.size() << " imports:" << std::endl; - for (const auto& import : imports) { - std::cout << " - " << import << std::endl; - } - print_separator(); - - // Example 4: TypeScript Interface Change Detection - std::cout << "Example 4: TypeScript Interface Change Detection" << std::endl; - std::cout << std::string(40, '-') << std::endl; - - std::vector base_interface = { - "interface User {", - " id: number;", - " name: string;", - "}" - }; - - std::vector modified_interface = { - "interface User {", - " id: number;", - " name: string;", - " email: string; // Added", - " age?: number; // Added optional", - "}" - }; - - bool has_ts_changes = has_typescript_interface_changes(base_interface, modified_interface); - std::cout << "Interface changed: " << (has_ts_changes ? "YES" : "NO") << std::endl; - std::cout << "Risk: Breaking change - affects all usages of User" << std::endl; - print_separator(); - - // Example 5: TypeScript Critical Pattern Detection - std::cout << "Example 5: TypeScript Critical Pattern Detection" << std::endl; - std::cout << std::string(40, '-') << std::endl; - - std::vector risky_code = { - "// Type safety bypass", - "const user = response.data as any;", - "", - "// Error suppression", - "// @ts-ignore", - "element.innerHTML = userInput;", - "", - "// Insecure storage", - "localStorage.setItem('password', pwd);" - }; - - bool has_critical = contains_critical_patterns(risky_code); - std::cout << "Contains critical patterns: " << (has_critical ? "YES" : "NO") << std::endl; - if (has_critical) { - std::cout << "Critical issues detected:" << std::endl; - std::cout << " - Type safety bypass (as any)" << std::endl; - std::cout << " - Error suppression (@ts-ignore)" << std::endl; - std::cout << " - XSS vulnerability (innerHTML)" << std::endl; - std::cout << " - Insecure password storage (localStorage)" << std::endl; - } - print_separator(); - - // Example 6: Package Lock File Detection - std::cout << "Example 6: Package Lock File Detection" << std::endl; - std::cout << std::string(40, '-') << std::endl; - - std::vector lock_files = { - "package-lock.json", - "yarn.lock", - "pnpm-lock.yaml", - "bun.lockb", - "package.json" - }; - - for (const auto& file : lock_files) { - bool is_lock = is_package_lock_file(file); - std::cout << file << ": " << (is_lock ? "LOCK FILE" : "regular file") << std::endl; - } - - std::cout << "\nRecommendation for lock file conflicts:" << std::endl; - std::cout << " 1. Merge package.json manually" << std::endl; - std::cout << " 2. Delete lock file" << std::endl; - std::cout << " 3. Run package manager to regenerate" << std::endl; - print_separator(); - - // Example 7: Complete Risk Analysis - std::cout << "Example 7: Complete Risk Analysis for TypeScript Changes" << std::endl; - std::cout << std::string(40, '-') << std::endl; - - std::vector base = { - "interface Config {", - " timeout: number;", - "}" - }; - - std::vector ours = { - "interface Config {", - " timeout: number;", - " retries: number;", - "}" - }; - - std::vector theirs = { - "interface Config {", - " timeout: number;", - "}" - }; - - auto risk = analyze_risk_ours(base, ours, theirs); - - std::cout << "Risk Level: " << risk_level_to_string(risk.level) << std::endl; - std::cout << "Confidence: " << risk.confidence_score << std::endl; - std::cout << "Has API Changes: " << (risk.has_api_changes ? "YES" : "NO") << std::endl; - - std::cout << "\nRisk Factors:" << std::endl; - for (const auto& factor : risk.risk_factors) { - std::cout << " - " << factor << std::endl; - } - - std::cout << "\nRecommendations:" << std::endl; - for (const auto& rec : risk.recommendations) { - std::cout << " - " << rec << std::endl; - } - print_separator(); - - std::cout << "Demo completed successfully!" << std::endl; - std::cout << "See docs/TYPESCRIPT_SUPPORT.md for more details." << std::endl; - - return 0; + std::cout << "WizardMerge TypeScript Support Demo" << std::endl; + print_separator(); + + // Example 1: TypeScript Function Detection + std::cout << "Example 1: TypeScript Function Detection" << std::endl; + std::cout << std::string(40, '-') << std::endl; + + std::vector ts_functions = { + "export async function fetchUser(id: number): Promise {", + " const response = await api.get(`/users/${id}`);", + " return response.data;", "}"}; + + std::string func_name = extract_function_name(ts_functions, 1); + std::cout << "Detected function: " << func_name << std::endl; + print_separator(); + + // Example 2: TypeScript Interface Detection + std::cout << "Example 2: TypeScript Interface Detection" << std::endl; + std::cout << std::string(40, '-') << std::endl; + + std::vector ts_interface = { + "export interface User {", " id: number;", " name: string;", + " email: string;", "}"}; + + std::string type_name = extract_class_name(ts_interface, 2); + std::cout << "Detected type: " << type_name << std::endl; + print_separator(); + + // Example 3: TypeScript Import Extraction + std::cout << "Example 3: TypeScript Import Extraction" << std::endl; + std::cout << std::string(40, '-') << std::endl; + + std::vector ts_imports = { + "import { Component, useState } from 'react';", + "import type { User } from './types';", + "import * as utils from './utils';", "", + "export const MyComponent = () => {"}; + + auto imports = extract_imports(ts_imports); + std::cout << "Detected " << imports.size() << " imports:" << std::endl; + for (const auto &import : imports) { + std::cout << " - " << import << std::endl; + } + print_separator(); + + // Example 4: TypeScript Interface Change Detection + std::cout << "Example 4: TypeScript Interface Change Detection" << std::endl; + std::cout << std::string(40, '-') << std::endl; + + std::vector base_interface = { + "interface User {", " id: number;", " name: string;", "}"}; + + std::vector modified_interface = { + "interface User {", + " id: number;", + " name: string;", + " email: string; // Added", + " age?: number; // Added optional", + "}"}; + + bool has_ts_changes = + has_typescript_interface_changes(base_interface, modified_interface); + std::cout << "Interface changed: " << (has_ts_changes ? "YES" : "NO") + << std::endl; + std::cout << "Risk: Breaking change - affects all usages of User" + << std::endl; + print_separator(); + + // Example 5: TypeScript Critical Pattern Detection + std::cout << "Example 5: TypeScript Critical Pattern Detection" << std::endl; + std::cout << std::string(40, '-') << std::endl; + + std::vector risky_code = { + "// Type safety bypass", + "const user = response.data as any;", + "", + "// Error suppression", + "// @ts-ignore", + "element.innerHTML = userInput;", + "", + "// Insecure storage", + "localStorage.setItem('password', pwd);"}; + + bool has_critical = contains_critical_patterns(risky_code); + std::cout << "Contains critical patterns: " << (has_critical ? "YES" : "NO") + << std::endl; + if (has_critical) { + std::cout << "Critical issues detected:" << std::endl; + std::cout << " - Type safety bypass (as any)" << std::endl; + std::cout << " - Error suppression (@ts-ignore)" << std::endl; + std::cout << " - XSS vulnerability (innerHTML)" << std::endl; + std::cout << " - Insecure password storage (localStorage)" << std::endl; + } + print_separator(); + + // Example 6: Package Lock File Detection + std::cout << "Example 6: Package Lock File Detection" << std::endl; + std::cout << std::string(40, '-') << std::endl; + + std::vector lock_files = {"package-lock.json", "yarn.lock", + "pnpm-lock.yaml", "bun.lockb", + "package.json"}; + + for (const auto &file : lock_files) { + bool is_lock = is_package_lock_file(file); + std::cout << file << ": " << (is_lock ? "LOCK FILE" : "regular file") + << std::endl; + } + + std::cout << "\nRecommendation for lock file conflicts:" << std::endl; + std::cout << " 1. Merge package.json manually" << std::endl; + std::cout << " 2. Delete lock file" << std::endl; + std::cout << " 3. Run package manager to regenerate" << std::endl; + print_separator(); + + // Example 7: Complete Risk Analysis + std::cout << "Example 7: Complete Risk Analysis for TypeScript Changes" + << std::endl; + std::cout << std::string(40, '-') << std::endl; + + std::vector base = {"interface Config {", " timeout: number;", + "}"}; + + std::vector ours = {"interface Config {", " timeout: number;", + " retries: number;", "}"}; + + std::vector theirs = {"interface Config {", + " timeout: number;", "}"}; + + auto risk = analyze_risk_ours(base, ours, theirs); + + std::cout << "Risk Level: " << risk_level_to_string(risk.level) << std::endl; + std::cout << "Confidence: " << risk.confidence_score << std::endl; + std::cout << "Has API Changes: " << (risk.has_api_changes ? "YES" : "NO") + << std::endl; + + std::cout << "\nRisk Factors:" << std::endl; + for (const auto &factor : risk.risk_factors) { + std::cout << " - " << factor << std::endl; + } + + std::cout << "\nRecommendations:" << std::endl; + for (const auto &rec : risk.recommendations) { + std::cout << " - " << rec << std::endl; + } + print_separator(); + + std::cout << "Demo completed successfully!" << std::endl; + std::cout << "See docs/TYPESCRIPT_SUPPORT.md for more details." << std::endl; + + return 0; } diff --git a/backend/include/wizardmerge/analysis/context_analyzer.h b/backend/include/wizardmerge/analysis/context_analyzer.h index f20362b..1146840 100644 --- a/backend/include/wizardmerge/analysis/context_analyzer.h +++ b/backend/include/wizardmerge/analysis/context_analyzer.h @@ -9,9 +9,9 @@ #ifndef WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H #define WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H +#include #include #include -#include namespace wizardmerge { namespace analysis { @@ -20,13 +20,13 @@ namespace analysis { * @brief Represents code context information for a specific line or region. */ struct CodeContext { - size_t start_line; - size_t end_line; - std::vector surrounding_lines; - std::string function_name; - std::string class_name; - std::vector imports; - std::map metadata; + size_t start_line; + size_t end_line; + std::vector surrounding_lines; + std::string function_name; + std::string class_name; + std::vector imports; + std::map metadata; }; /** @@ -42,12 +42,9 @@ struct CodeContext { * @param context_window Number of lines before/after to include (default: 5) * @return CodeContext containing analyzed context information */ -CodeContext analyze_context( - const std::vector& lines, - size_t start_line, - size_t end_line, - size_t context_window = 5 -); +CodeContext analyze_context(const std::vector &lines, + size_t start_line, size_t end_line, + size_t context_window = 5); /** * @brief Extracts function or method name from context. @@ -59,10 +56,8 @@ CodeContext analyze_context( * @param line_number Line number to check * @return Function name if found, empty string otherwise */ -std::string extract_function_name( - const std::vector& lines, - size_t line_number -); +std::string extract_function_name(const std::vector &lines, + size_t line_number); /** * @brief Extracts class name from context. @@ -74,10 +69,8 @@ std::string extract_function_name( * @param line_number Line number to check * @return Class name if found, empty string otherwise */ -std::string extract_class_name( - const std::vector& lines, - size_t line_number -); +std::string extract_class_name(const std::vector &lines, + size_t line_number); /** * @brief Extracts import/include statements from the file. @@ -88,11 +81,9 @@ std::string extract_class_name( * @param lines Lines of code to analyze * @return Vector of import statements */ -std::vector extract_imports( - const std::vector& lines -); +std::vector extract_imports(const std::vector &lines); -} // namespace analysis -} // namespace wizardmerge +} // namespace analysis +} // namespace wizardmerge -#endif // WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H +#endif // WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H diff --git a/backend/include/wizardmerge/analysis/risk_analyzer.h b/backend/include/wizardmerge/analysis/risk_analyzer.h index 614a909..a6a86d1 100644 --- a/backend/include/wizardmerge/analysis/risk_analyzer.h +++ b/backend/include/wizardmerge/analysis/risk_analyzer.h @@ -19,27 +19,27 @@ namespace analysis { * @brief Risk level enumeration for merge resolutions. */ enum class RiskLevel { - LOW, // Safe to merge, minimal risk - MEDIUM, // Some risk, review recommended - HIGH, // High risk, careful review required - CRITICAL // Critical risk, requires expert review + LOW, // Safe to merge, minimal risk + MEDIUM, // Some risk, review recommended + HIGH, // High risk, careful review required + CRITICAL // Critical risk, requires expert review }; /** * @brief Detailed risk assessment for a merge resolution. */ struct RiskAssessment { - RiskLevel level; - double confidence_score; // 0.0 to 1.0 - std::vector risk_factors; - std::vector recommendations; - - // Specific risk indicators - bool has_syntax_changes; - bool has_logic_changes; - bool has_api_changes; - bool affects_multiple_functions; - bool affects_critical_section; + RiskLevel level; + double confidence_score; // 0.0 to 1.0 + std::vector risk_factors; + std::vector recommendations; + + // Specific risk indicators + bool has_syntax_changes; + bool has_logic_changes; + bool has_api_changes; + bool affects_multiple_functions; + bool affects_critical_section; }; /** @@ -50,11 +50,9 @@ struct RiskAssessment { * @param theirs Their version lines * @return RiskAssessment for accepting ours */ -RiskAssessment analyze_risk_ours( - const std::vector& base, - const std::vector& ours, - const std::vector& theirs -); +RiskAssessment analyze_risk_ours(const std::vector &base, + const std::vector &ours, + const std::vector &theirs); /** * @brief Analyzes risk of accepting "theirs" version. @@ -64,11 +62,9 @@ RiskAssessment analyze_risk_ours( * @param theirs Their version lines * @return RiskAssessment for accepting theirs */ -RiskAssessment analyze_risk_theirs( - const std::vector& base, - const std::vector& ours, - const std::vector& theirs -); +RiskAssessment analyze_risk_theirs(const std::vector &base, + const std::vector &ours, + const std::vector &theirs); /** * @brief Analyzes risk of accepting both versions (concatenation). @@ -78,11 +74,9 @@ RiskAssessment analyze_risk_theirs( * @param theirs Their version lines * @return RiskAssessment for accepting both */ -RiskAssessment analyze_risk_both( - const std::vector& base, - const std::vector& ours, - const std::vector& theirs -); +RiskAssessment analyze_risk_both(const std::vector &base, + const std::vector &ours, + const std::vector &theirs); /** * @brief Converts RiskLevel to string representation. @@ -98,7 +92,7 @@ std::string risk_level_to_string(RiskLevel level); * @param lines Lines of code to check * @return true if critical patterns detected */ -bool contains_critical_patterns(const std::vector& lines); +bool contains_critical_patterns(const std::vector &lines); /** * @brief Detects if changes affect API signatures. @@ -107,10 +101,8 @@ bool contains_critical_patterns(const std::vector& lines); * @param modified Modified version lines * @return true if API changes detected */ -bool has_api_signature_changes( - const std::vector& base, - const std::vector& modified -); +bool has_api_signature_changes(const std::vector &base, + const std::vector &modified); /** * @brief Detects if TypeScript interface or type definitions changed. @@ -119,10 +111,8 @@ bool has_api_signature_changes( * @param modified Modified version lines * @return true if interface/type changes detected */ -bool has_typescript_interface_changes( - const std::vector& base, - const std::vector& modified -); +bool has_typescript_interface_changes(const std::vector &base, + const std::vector &modified); /** * @brief Checks if file is a package-lock.json file. @@ -130,9 +120,9 @@ bool has_typescript_interface_changes( * @param filename Name of the file * @return true if file is package-lock.json */ -bool is_package_lock_file(const std::string& filename); +bool is_package_lock_file(const std::string &filename); -} // namespace analysis -} // namespace wizardmerge +} // namespace analysis +} // namespace wizardmerge -#endif // WIZARDMERGE_ANALYSIS_RISK_ANALYZER_H +#endif // WIZARDMERGE_ANALYSIS_RISK_ANALYZER_H diff --git a/backend/include/wizardmerge/git/git_cli.h b/backend/include/wizardmerge/git/git_cli.h index 46de069..e30ed3f 100644 --- a/backend/include/wizardmerge/git/git_cli.h +++ b/backend/include/wizardmerge/git/git_cli.h @@ -1,7 +1,7 @@ /** * @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. */ @@ -9,9 +9,9 @@ #ifndef WIZARDMERGE_GIT_CLI_H #define WIZARDMERGE_GIT_CLI_H +#include #include #include -#include namespace wizardmerge { namespace git { @@ -20,92 +20,80 @@ namespace git { * @brief Result of a Git operation */ struct GitResult { - bool success; - std::string output; - std::string error; - int exit_code; + 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 + 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 -); +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 = "" -); +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 -); +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& files -); +GitResult add_files(const std::string &repo_path, + const std::vector &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() -); +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 @@ -113,47 +101,44 @@ GitResult commit( * @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() -); +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 get_current_branch(const std::string& repo_path); +std::optional 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); +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); +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 +} // namespace git +} // namespace wizardmerge -#endif // WIZARDMERGE_GIT_CLI_H +#endif // WIZARDMERGE_GIT_CLI_H diff --git a/backend/include/wizardmerge/git/git_platform_client.h b/backend/include/wizardmerge/git/git_platform_client.h index c2ba3fa..2a0a5ae 100644 --- a/backend/include/wizardmerge/git/git_platform_client.h +++ b/backend/include/wizardmerge/git/git_platform_client.h @@ -1,17 +1,17 @@ /** * @file git_platform_client.h * @brief Git platform API client for fetching pull/merge request information - * + * * Supports GitHub and GitLab platforms */ #ifndef WIZARDMERGE_GIT_PLATFORM_CLIENT_H #define WIZARDMERGE_GIT_PLATFORM_CLIENT_H -#include -#include #include #include +#include +#include namespace wizardmerge { namespace git { @@ -19,51 +19,47 @@ namespace git { /** * @brief Supported git platforms */ -enum class GitPlatform { - GitHub, - GitLab, - Unknown -}; +enum class GitPlatform { GitHub, GitLab, Unknown }; /** * @brief Information about a file in a pull/merge request */ struct PRFile { - std::string filename; - std::string status; // "added", "modified", "removed", "renamed" - int additions; - int deletions; - int changes; + std::string filename; + std::string status; // "added", "modified", "removed", "renamed" + int additions; + int deletions; + int changes; }; /** * @brief Pull/merge request information from GitHub or GitLab */ struct PullRequest { - GitPlatform platform; - 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 files; - bool mergeable; - std::string mergeable_state; + GitPlatform platform; + 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 files; + bool mergeable; + std::string mergeable_state; }; /** * @brief Parse pull/merge request URL - * + * * Extracts platform, owner, repo, and PR/MR number from URLs like: * - https://github.com/owner/repo/pull/123 * - https://gitlab.com/owner/repo/-/merge_requests/456 * - github.com/owner/repo/pull/123 * - gitlab.com/group/subgroup/project/-/merge_requests/789 - * + * * @param url The pull/merge request URL * @param platform Output git platform * @param owner Output repository owner/group @@ -71,12 +67,12 @@ struct PullRequest { * @param pr_number Output PR/MR number * @return true if successfully parsed, false otherwise */ -bool parse_pr_url(const std::string& url, GitPlatform& platform, - std::string& owner, std::string& repo, int& pr_number); +bool parse_pr_url(const std::string &url, GitPlatform &platform, + std::string &owner, std::string &repo, int &pr_number); /** * @brief Fetch pull/merge request information from GitHub or GitLab API - * + * * @param platform Git platform (GitHub or GitLab) * @param owner Repository owner/group * @param repo Repository name/project @@ -84,17 +80,15 @@ bool parse_pr_url(const std::string& url, GitPlatform& platform, * @param token Optional API token for authentication * @return Pull request information, or empty optional on error */ -std::optional fetch_pull_request( - GitPlatform platform, - const std::string& owner, - const std::string& repo, - int pr_number, - const std::string& token = "" -); +std::optional fetch_pull_request(GitPlatform platform, + const std::string &owner, + const std::string &repo, + int pr_number, + const std::string &token = ""); /** * @brief Fetch file content from GitHub or GitLab at a specific commit - * + * * @param platform Git platform (GitHub or GitLab) * @param owner Repository owner/group * @param repo Repository name/project @@ -103,16 +97,12 @@ std::optional fetch_pull_request( * @param token Optional API token * @return File content as vector of lines, or empty optional on error */ -std::optional> fetch_file_content( - GitPlatform platform, - const std::string& owner, - const std::string& repo, - const std::string& sha, - const std::string& path, - const std::string& token = "" -); +std::optional> +fetch_file_content(GitPlatform platform, const std::string &owner, + const std::string &repo, const std::string &sha, + const std::string &path, const std::string &token = ""); -} // namespace git -} // namespace wizardmerge +} // namespace git +} // namespace wizardmerge -#endif // WIZARDMERGE_GIT_PLATFORM_CLIENT_H +#endif // WIZARDMERGE_GIT_PLATFORM_CLIENT_H diff --git a/backend/include/wizardmerge/merge/three_way_merge.h b/backend/include/wizardmerge/merge/three_way_merge.h index 77f1953..0a2ecd6 100644 --- a/backend/include/wizardmerge/merge/three_way_merge.h +++ b/backend/include/wizardmerge/merge/three_way_merge.h @@ -10,10 +10,10 @@ #ifndef WIZARDMERGE_MERGE_THREE_WAY_MERGE_H #define WIZARDMERGE_MERGE_THREE_WAY_MERGE_H -#include -#include #include "wizardmerge/analysis/context_analyzer.h" #include "wizardmerge/analysis/risk_analyzer.h" +#include +#include namespace wizardmerge { namespace merge { @@ -22,34 +22,34 @@ namespace merge { * @brief Represents a single line in a file with its origin. */ struct Line { - std::string content; - enum Origin { BASE, OURS, THEIRS, MERGED } origin; + std::string content; + enum Origin { BASE, OURS, THEIRS, MERGED } origin; }; /** * @brief Represents a conflict region in the merge result. */ struct Conflict { - size_t start_line; - size_t end_line; - std::vector base_lines; - std::vector our_lines; - std::vector their_lines; - - // Context and risk analysis - analysis::CodeContext context; - analysis::RiskAssessment risk_ours; - analysis::RiskAssessment risk_theirs; - analysis::RiskAssessment risk_both; + size_t start_line; + size_t end_line; + std::vector base_lines; + std::vector our_lines; + std::vector their_lines; + + // Context and risk analysis + analysis::CodeContext context; + analysis::RiskAssessment risk_ours; + analysis::RiskAssessment risk_theirs; + analysis::RiskAssessment risk_both; }; /** * @brief Result of a three-way merge operation. */ struct MergeResult { - std::vector merged_lines; - std::vector conflicts; - bool has_conflicts() const { return !conflicts.empty(); } + std::vector merged_lines; + std::vector conflicts; + bool has_conflicts() const { return !conflicts.empty(); } }; /** @@ -65,11 +65,9 @@ struct MergeResult { * @param theirs Their version (branch being merged) * @return MergeResult containing the merged content and any conflicts */ -MergeResult three_way_merge( - const std::vector& base, - const std::vector& ours, - const std::vector& theirs -); +MergeResult three_way_merge(const std::vector &base, + const std::vector &ours, + const std::vector &theirs); /** * @brief Auto-resolves simple non-conflicting patterns. @@ -82,9 +80,9 @@ MergeResult three_way_merge( * @param result The merge result to auto-resolve * @return Updated merge result with resolved conflicts */ -MergeResult auto_resolve(const MergeResult& result); +MergeResult auto_resolve(const MergeResult &result); -} // namespace merge -} // namespace wizardmerge +} // namespace merge +} // namespace wizardmerge -#endif // WIZARDMERGE_MERGE_THREE_WAY_MERGE_H +#endif // WIZARDMERGE_MERGE_THREE_WAY_MERGE_H diff --git a/backend/src/analysis/context_analyzer.cpp b/backend/src/analysis/context_analyzer.cpp index 9935ecd..b1c0ed5 100644 --- a/backend/src/analysis/context_analyzer.cpp +++ b/backend/src/analysis/context_analyzer.cpp @@ -18,241 +18,244 @@ constexpr size_t IMPORT_SCAN_LIMIT = 50; /** * @brief Trim whitespace from string. */ -std::string trim(const std::string& str) { - size_t start = str.find_first_not_of(" \t\n\r"); - size_t end = str.find_last_not_of(" \t\n\r"); - if (start == std::string::npos) return ""; - return str.substr(start, end - start + 1); +std::string trim(const std::string &str) { + size_t start = str.find_first_not_of(" \t\n\r"); + size_t end = str.find_last_not_of(" \t\n\r"); + if (start == std::string::npos) + return ""; + return str.substr(start, end - start + 1); } /** * @brief Check if a line is a function definition. */ -bool is_function_definition(const std::string& line) { - std::string trimmed = trim(line); - - // Common function patterns across languages - std::vector patterns = { - std::regex(R"(^\w+\s+\w+\s*\([^)]*\)\s*\{?)"), // C/C++/Java: type name(params) - std::regex(R"(^def\s+\w+\s*\([^)]*\):)"), // Python: def name(params): - std::regex(R"(^function\s+\w+\s*\([^)]*\))"), // JavaScript: function name(params) - std::regex(R"(^\w+\s*:\s*function\s*\([^)]*\))"), // JS object method - std::regex(R"(^(public|private|protected)?\s*\w+\s+\w+\s*\([^)]*\))"), // Java/C# methods - // TypeScript patterns - std::regex(R"(^(export\s+)?(async\s+)?function\s+\w+)"), // TS: export/async function - std::regex(R"(^(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>)"), // TS: arrow functions - std::regex(R"(^(public|private|protected|readonly)?\s*\w+\s*\([^)]*\)\s*:\s*\w+)") // TS: typed methods - }; - - for (const auto& pattern : patterns) { - if (std::regex_search(trimmed, pattern)) { - return true; - } +bool is_function_definition(const std::string &line) { + std::string trimmed = trim(line); + + // Common function patterns across languages + std::vector patterns = { + std::regex( + R"(^\w+\s+\w+\s*\([^)]*\)\s*\{?)"), // C/C++/Java: type name(params) + std::regex(R"(^def\s+\w+\s*\([^)]*\):)"), // Python: def name(params): + std::regex(R"(^function\s+\w+\s*\([^)]*\))"), // JavaScript: function + // name(params) + std::regex(R"(^\w+\s*:\s*function\s*\([^)]*\))"), // JS object method + std::regex( + R"(^(public|private|protected)?\s*\w+\s+\w+\s*\([^)]*\))"), // Java/C# + // methods + // TypeScript patterns + std::regex( + R"(^(export\s+)?(async\s+)?function\s+\w+)"), // TS: export/async + // function + std::regex( + R"(^(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>)"), // TS: arrow functions + std::regex( + R"(^(public|private|protected|readonly)?\s*\w+\s*\([^)]*\)\s*:\s*\w+)") // TS: typed methods + }; + + for (const auto &pattern : patterns) { + if (std::regex_search(trimmed, pattern)) { + return true; } - - return false; + } + + return false; } /** * @brief Extract function name from a function definition line. */ -std::string get_function_name_from_line(const std::string& line) { - std::string trimmed = trim(line); - - // Try to extract function name using regex - std::smatch match; - - // Python: def function_name( - std::regex py_pattern(R"(def\s+(\w+)\s*\()"); - if (std::regex_search(trimmed, match, py_pattern)) { - return match[1].str(); - } - - // JavaScript/TypeScript: function function_name( or export function function_name( - std::regex js_pattern(R"((?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\()"); - if (std::regex_search(trimmed, match, js_pattern)) { - return match[1].str(); - } - - // TypeScript: const/let/var function_name = (params) => - std::regex arrow_pattern(R"((?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)"); - if (std::regex_search(trimmed, match, arrow_pattern)) { - return match[1].str(); - } - - // C/C++/Java: type function_name( - std::regex cpp_pattern(R"(\w+\s+(\w+)\s*\()"); - if (std::regex_search(trimmed, match, cpp_pattern)) { - return match[1].str(); - } - - return ""; +std::string get_function_name_from_line(const std::string &line) { + std::string trimmed = trim(line); + + // Try to extract function name using regex + std::smatch match; + + // Python: def function_name( + std::regex py_pattern(R"(def\s+(\w+)\s*\()"); + if (std::regex_search(trimmed, match, py_pattern)) { + return match[1].str(); + } + + // JavaScript/TypeScript: function function_name( or export function + // function_name( + std::regex js_pattern(R"((?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\()"); + if (std::regex_search(trimmed, match, js_pattern)) { + return match[1].str(); + } + + // TypeScript: const/let/var function_name = (params) => + std::regex arrow_pattern( + R"((?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)"); + if (std::regex_search(trimmed, match, arrow_pattern)) { + return match[1].str(); + } + + // C/C++/Java: type function_name( + std::regex cpp_pattern(R"(\w+\s+(\w+)\s*\()"); + if (std::regex_search(trimmed, match, cpp_pattern)) { + return match[1].str(); + } + + return ""; } /** * @brief Check if a line is a class definition. */ -bool is_class_definition(const std::string& line) { - std::string trimmed = trim(line); - - std::vector patterns = { - std::regex(R"(^class\s+\w+)"), // Python/C++/Java: class Name - std::regex(R"(^(public|private)?\s*class\s+\w+)"), // Java/C#: visibility class Name - std::regex(R"(^struct\s+\w+)"), // C/C++: struct Name - // TypeScript patterns - std::regex(R"(^(export\s+)?(abstract\s+)?class\s+\w+)"), // TS: export class Name - std::regex(R"(^(export\s+)?interface\s+\w+)"), // TS: interface Name - std::regex(R"(^(export\s+)?type\s+\w+\s*=)"), // TS: type Name = - std::regex(R"(^(export\s+)?enum\s+\w+)") // TS: enum Name - }; - - for (const auto& pattern : patterns) { - if (std::regex_search(trimmed, pattern)) { - return true; - } +bool is_class_definition(const std::string &line) { + std::string trimmed = trim(line); + + std::vector patterns = { + std::regex(R"(^class\s+\w+)"), // Python/C++/Java: class Name + std::regex(R"(^(public|private)?\s*class\s+\w+)"), // Java/C#: visibility + // class Name + std::regex(R"(^struct\s+\w+)"), // C/C++: struct Name + // TypeScript patterns + std::regex( + R"(^(export\s+)?(abstract\s+)?class\s+\w+)"), // TS: export class Name + std::regex(R"(^(export\s+)?interface\s+\w+)"), // TS: interface Name + std::regex(R"(^(export\s+)?type\s+\w+\s*=)"), // TS: type Name = + std::regex(R"(^(export\s+)?enum\s+\w+)") // TS: enum Name + }; + + for (const auto &pattern : patterns) { + if (std::regex_search(trimmed, pattern)) { + return true; } - - return false; + } + + return false; } /** * @brief Extract class name from a class definition line. */ -std::string get_class_name_from_line(const std::string& line) { - std::string trimmed = trim(line); - - std::smatch match; - - // Match class, struct, interface, type, or enum - std::regex pattern(R"((?:export\s+)?(?:abstract\s+)?(class|struct|interface|type|enum)\s+(\w+))"); - - if (std::regex_search(trimmed, match, pattern)) { - return match[2].str(); - } - +std::string get_class_name_from_line(const std::string &line) { + std::string trimmed = trim(line); + + std::smatch match; + + // Match class, struct, interface, type, or enum + std::regex pattern( + R"((?:export\s+)?(?:abstract\s+)?(class|struct|interface|type|enum)\s+(\w+))"); + + if (std::regex_search(trimmed, match, pattern)) { + return match[2].str(); + } + + return ""; +} + +} // anonymous namespace + +CodeContext analyze_context(const std::vector &lines, + size_t start_line, size_t end_line, + size_t context_window) { + CodeContext context; + context.start_line = start_line; + context.end_line = end_line; + + // Extract surrounding lines + size_t window_start = + (start_line >= context_window) ? (start_line - context_window) : 0; + size_t window_end = std::min(end_line + context_window, lines.size()); + + for (size_t i = window_start; i < window_end; ++i) { + context.surrounding_lines.push_back(lines[i]); + } + + // Extract function name + context.function_name = extract_function_name(lines, start_line); + + // Extract class name + context.class_name = extract_class_name(lines, start_line); + + // Extract imports + context.imports = extract_imports(lines); + + // Add metadata + context.metadata["context_window_start"] = std::to_string(window_start); + context.metadata["context_window_end"] = std::to_string(window_end); + context.metadata["total_lines"] = std::to_string(lines.size()); + + return context; +} + +std::string extract_function_name(const std::vector &lines, + size_t line_number) { + if (line_number >= lines.size()) { return ""; + } + + // Check the line itself first + if (is_function_definition(lines[line_number])) { + return get_function_name_from_line(lines[line_number]); + } + + // Search backwards for function definition + for (int i = static_cast(line_number) - 1; i >= 0; --i) { + if (is_function_definition(lines[i])) { + return get_function_name_from_line(lines[i]); + } + + // Stop searching if we hit a class definition or another function + std::string trimmed = trim(lines[i]); + if (trimmed.find("class ") == 0 || trimmed.find("struct ") == 0) { + break; + } + } + + return ""; } -} // anonymous namespace - -CodeContext analyze_context( - const std::vector& lines, - size_t start_line, - size_t end_line, - size_t context_window -) { - CodeContext context; - context.start_line = start_line; - context.end_line = end_line; - - // Extract surrounding lines - size_t window_start = (start_line >= context_window) ? (start_line - context_window) : 0; - size_t window_end = std::min(end_line + context_window, lines.size()); - - for (size_t i = window_start; i < window_end; ++i) { - context.surrounding_lines.push_back(lines[i]); - } - - // Extract function name - context.function_name = extract_function_name(lines, start_line); - - // Extract class name - context.class_name = extract_class_name(lines, start_line); - - // Extract imports - context.imports = extract_imports(lines); - - // Add metadata - context.metadata["context_window_start"] = std::to_string(window_start); - context.metadata["context_window_end"] = std::to_string(window_end); - context.metadata["total_lines"] = std::to_string(lines.size()); - - return context; -} - -std::string extract_function_name( - const std::vector& lines, - size_t line_number -) { - if (line_number >= lines.size()) { - return ""; - } - - // Check the line itself first - if (is_function_definition(lines[line_number])) { - return get_function_name_from_line(lines[line_number]); - } - - // Search backwards for function definition - for (int i = static_cast(line_number) - 1; i >= 0; --i) { - if (is_function_definition(lines[i])) { - return get_function_name_from_line(lines[i]); - } - - // Stop searching if we hit a class definition or another function - std::string trimmed = trim(lines[i]); - if (trimmed.find("class ") == 0 || trimmed.find("struct ") == 0) { - break; - } - } - +std::string extract_class_name(const std::vector &lines, + size_t line_number) { + if (line_number >= lines.size()) { return ""; + } + + // Search backwards for class definition + int brace_count = 0; + for (int i = static_cast(line_number); i >= 0; --i) { + std::string line = lines[i]; + + // Count braces to track scope + brace_count += std::count(line.begin(), line.end(), '}'); + brace_count -= std::count(line.begin(), line.end(), '{'); + + if (is_class_definition(line) && brace_count <= 0) { + return get_class_name_from_line(line); + } + } + + return ""; } -std::string extract_class_name( - const std::vector& lines, - size_t line_number -) { - if (line_number >= lines.size()) { - return ""; +std::vector +extract_imports(const std::vector &lines) { + std::vector imports; + + // Scan first lines for imports (imports are typically at the top) + size_t scan_limit = std::min(lines.size(), IMPORT_SCAN_LIMIT); + + for (size_t i = 0; i < scan_limit; ++i) { + std::string line = trim(lines[i]); + + // Check for various import patterns + if (line.find("#include") == 0 || line.find("import ") == 0 || + line.find("import{") == 0 || // Support both "import{" and "import {" + line.find("from ") == 0 || line.find("require(") != std::string::npos || + line.find("using ") == 0 || + // TypeScript/ES6 specific patterns + line.find("import *") == 0 || line.find("import type") == 0 || + line.find("export {") == 0 || line.find("export *") == 0) { + imports.push_back(line); } - - // Search backwards for class definition - int brace_count = 0; - for (int i = static_cast(line_number); i >= 0; --i) { - std::string line = lines[i]; - - // Count braces to track scope - brace_count += std::count(line.begin(), line.end(), '}'); - brace_count -= std::count(line.begin(), line.end(), '{'); - - if (is_class_definition(line) && brace_count <= 0) { - return get_class_name_from_line(line); - } - } - - return ""; + } + + return imports; } -std::vector extract_imports( - const std::vector& lines -) { - std::vector imports; - - // Scan first lines for imports (imports are typically at the top) - size_t scan_limit = std::min(lines.size(), IMPORT_SCAN_LIMIT); - - for (size_t i = 0; i < scan_limit; ++i) { - std::string line = trim(lines[i]); - - // Check for various import patterns - if (line.find("#include") == 0 || - line.find("import ") == 0 || - line.find("import{") == 0 || // Support both "import{" and "import {" - line.find("from ") == 0 || - line.find("require(") != std::string::npos || - line.find("using ") == 0 || - // TypeScript/ES6 specific patterns - line.find("import *") == 0 || - line.find("import type") == 0 || - line.find("export {") == 0 || - line.find("export *") == 0) { - imports.push_back(line); - } - } - - return imports; -} - -} // namespace analysis -} // namespace wizardmerge +} // namespace analysis +} // namespace wizardmerge diff --git a/backend/src/analysis/risk_analyzer.cpp b/backend/src/analysis/risk_analyzer.cpp index f211e41..369dfad 100644 --- a/backend/src/analysis/risk_analyzer.cpp +++ b/backend/src/analysis/risk_analyzer.cpp @@ -5,8 +5,8 @@ #include "wizardmerge/analysis/risk_analyzer.h" #include -#include #include +#include namespace wizardmerge { namespace analysis { @@ -14,439 +14,470 @@ namespace analysis { namespace { // Confidence score weights for risk assessment -constexpr double BASE_CONFIDENCE = 0.5; // Base confidence level -constexpr double SIMILARITY_WEIGHT = 0.3; // Weight for code similarity -constexpr double CHANGE_RATIO_WEIGHT = 0.2; // Weight for change ratio +constexpr double BASE_CONFIDENCE = 0.5; // Base confidence level +constexpr double SIMILARITY_WEIGHT = 0.3; // Weight for code similarity +constexpr double CHANGE_RATIO_WEIGHT = 0.2; // Weight for change ratio /** * @brief Trim whitespace from string. */ -std::string trim(const std::string& str) { - size_t start = str.find_first_not_of(" \t\n\r"); - size_t end = str.find_last_not_of(" \t\n\r"); - if (start == std::string::npos) return ""; - return str.substr(start, end - start + 1); +std::string trim(const std::string &str) { + size_t start = str.find_first_not_of(" \t\n\r"); + size_t end = str.find_last_not_of(" \t\n\r"); + if (start == std::string::npos) + return ""; + return str.substr(start, end - start + 1); } /** * @brief Calculate similarity score between two sets of lines (0.0 to 1.0). */ -double calculate_similarity( - const std::vector& lines1, - const std::vector& lines2 -) { - if (lines1.empty() && lines2.empty()) return 1.0; - if (lines1.empty() || lines2.empty()) return 0.0; - - // Simple Jaccard similarity on lines - size_t common_lines = 0; - for (const auto& line1 : lines1) { - if (std::find(lines2.begin(), lines2.end(), line1) != lines2.end()) { - common_lines++; - } +double calculate_similarity(const std::vector &lines1, + const std::vector &lines2) { + if (lines1.empty() && lines2.empty()) + return 1.0; + if (lines1.empty() || lines2.empty()) + return 0.0; + + // Simple Jaccard similarity on lines + size_t common_lines = 0; + for (const auto &line1 : lines1) { + if (std::find(lines2.begin(), lines2.end(), line1) != lines2.end()) { + common_lines++; } - - size_t total_unique = lines1.size() + lines2.size() - common_lines; - return total_unique > 0 ? static_cast(common_lines) / total_unique : 0.0; + } + + size_t total_unique = lines1.size() + lines2.size() - common_lines; + return total_unique > 0 ? static_cast(common_lines) / total_unique + : 0.0; } /** * @brief Count number of changed lines between two versions. */ -size_t count_changes( - const std::vector& base, - const std::vector& modified -) { - size_t changes = 0; - size_t max_len = std::max(base.size(), modified.size()); - - for (size_t i = 0; i < max_len; ++i) { - std::string base_line = (i < base.size()) ? base[i] : ""; - std::string mod_line = (i < modified.size()) ? modified[i] : ""; - - if (base_line != mod_line) { - changes++; - } +size_t count_changes(const std::vector &base, + const std::vector &modified) { + size_t changes = 0; + size_t max_len = std::max(base.size(), modified.size()); + + for (size_t i = 0; i < max_len; ++i) { + std::string base_line = (i < base.size()) ? base[i] : ""; + std::string mod_line = (i < modified.size()) ? modified[i] : ""; + + if (base_line != mod_line) { + changes++; } - - return changes; + } + + return changes; } /** * @brief Check if line contains function or method definition. */ -bool is_function_signature(const std::string& line) { - std::string trimmed = trim(line); - - std::vector patterns = { - std::regex(R"(^\w+\s+\w+\s*\([^)]*\))"), // C/C++/Java - std::regex(R"(^def\s+\w+\s*\([^)]*\):)"), // Python - std::regex(R"(^function\s+\w+\s*\([^)]*\))"), // JavaScript - // TypeScript patterns - std::regex(R"(^(export\s+)?(async\s+)?function\s+\w+\s*\([^)]*\))"), // TS function - std::regex(R"(^(const|let|var)\s+\w+\s*=\s*\([^)]*\)\s*=>)"), // Arrow function - std::regex(R"(^\w+\s*\([^)]*\)\s*:\s*\w+)"), // TS: method with return type - }; - - for (const auto& pattern : patterns) { - if (std::regex_search(trimmed, pattern)) { - return true; - } +bool is_function_signature(const std::string &line) { + std::string trimmed = trim(line); + + std::vector patterns = { + std::regex(R"(^\w+\s+\w+\s*\([^)]*\))"), // C/C++/Java + std::regex(R"(^def\s+\w+\s*\([^)]*\):)"), // Python + std::regex(R"(^function\s+\w+\s*\([^)]*\))"), // JavaScript + // TypeScript patterns + std::regex( + R"(^(export\s+)?(async\s+)?function\s+\w+\s*\([^)]*\))"), // TS + // function + std::regex( + R"(^(const|let|var)\s+\w+\s*=\s*\([^)]*\)\s*=>)"), // Arrow function + std::regex( + R"(^\w+\s*\([^)]*\)\s*:\s*\w+)"), // TS: method with return type + }; + + for (const auto &pattern : patterns) { + if (std::regex_search(trimmed, pattern)) { + return true; } - - return false; + } + + return false; } -} // anonymous namespace +} // anonymous namespace std::string risk_level_to_string(RiskLevel level) { - switch (level) { - case RiskLevel::LOW: return "low"; - case RiskLevel::MEDIUM: return "medium"; - case RiskLevel::HIGH: return "high"; - case RiskLevel::CRITICAL: return "critical"; - default: return "unknown"; - } + switch (level) { + case RiskLevel::LOW: + return "low"; + case RiskLevel::MEDIUM: + return "medium"; + case RiskLevel::HIGH: + return "high"; + case RiskLevel::CRITICAL: + return "critical"; + default: + return "unknown"; + } } -bool contains_critical_patterns(const std::vector& lines) { - std::vector critical_patterns = { - std::regex(R"(delete\s+\w+)"), // Delete operations - std::regex(R"(drop\s+(table|database))"), // Database drops - std::regex(R"(rm\s+-rf)"), // Destructive file operations - std::regex(R"(eval\s*\()"), // Eval (security risk) - std::regex(R"(exec\s*\()"), // Exec (security risk) - std::regex(R"(system\s*\()"), // System calls - std::regex(R"(\.password\s*=)"), // Password assignments - std::regex(R"(\.secret\s*=)"), // Secret assignments - std::regex(R"(sudo\s+)"), // Sudo usage - std::regex(R"(chmod\s+777)"), // Overly permissive permissions - // TypeScript specific critical patterns - std::regex(R"(dangerouslySetInnerHTML)"), // React XSS risk - std::regex(R"(\bas\s+any\b)"), // TypeScript: type safety bypass - std::regex(R"(@ts-ignore)"), // TypeScript: error suppression - std::regex(R"(@ts-nocheck)"), // TypeScript: file-level error suppression - std::regex(R"(localStorage\.setItem.*password)"), // Storing passwords in localStorage - std::regex(R"(innerHTML\s*=)"), // XSS risk - }; - - for (const auto& line : lines) { - std::string trimmed = trim(line); - for (const auto& pattern : critical_patterns) { - if (std::regex_search(trimmed, pattern)) { - return true; - } - } +bool contains_critical_patterns(const std::vector &lines) { + std::vector critical_patterns = { + std::regex(R"(delete\s+\w+)"), // Delete operations + std::regex(R"(drop\s+(table|database))"), // Database drops + std::regex(R"(rm\s+-rf)"), // Destructive file operations + std::regex(R"(eval\s*\()"), // Eval (security risk) + std::regex(R"(exec\s*\()"), // Exec (security risk) + std::regex(R"(system\s*\()"), // System calls + std::regex(R"(\.password\s*=)"), // Password assignments + std::regex(R"(\.secret\s*=)"), // Secret assignments + std::regex(R"(sudo\s+)"), // Sudo usage + std::regex(R"(chmod\s+777)"), // Overly permissive permissions + // TypeScript specific critical patterns + std::regex(R"(dangerouslySetInnerHTML)"), // React XSS risk + std::regex(R"(\bas\s+any\b)"), // TypeScript: type safety bypass + std::regex(R"(@ts-ignore)"), // TypeScript: error suppression + std::regex(R"(@ts-nocheck)"), // TypeScript: file-level error suppression + std::regex(R"(localStorage\.setItem.*password)"), // Storing passwords in + // localStorage + std::regex(R"(innerHTML\s*=)"), // XSS risk + }; + + for (const auto &line : lines) { + std::string trimmed = trim(line); + for (const auto &pattern : critical_patterns) { + if (std::regex_search(trimmed, pattern)) { + return true; + } } - - return false; + } + + return false; } -bool has_api_signature_changes( - const std::vector& base, - const std::vector& modified -) { - // Check if function signatures changed - for (size_t i = 0; i < base.size() && i < modified.size(); ++i) { - bool base_is_sig = is_function_signature(base[i]); - bool mod_is_sig = is_function_signature(modified[i]); - - if (base_is_sig && mod_is_sig && base[i] != modified[i]) { - return true; - } +bool has_api_signature_changes(const std::vector &base, + const std::vector &modified) { + // Check if function signatures changed + for (size_t i = 0; i < base.size() && i < modified.size(); ++i) { + bool base_is_sig = is_function_signature(base[i]); + bool mod_is_sig = is_function_signature(modified[i]); + + if (base_is_sig && mod_is_sig && base[i] != modified[i]) { + return true; } - - return false; + } + + return false; } bool has_typescript_interface_changes( - const std::vector& base, - const std::vector& modified -) { - // Use static regex patterns to avoid recompilation - static const std::vector ts_definition_patterns = { - std::regex(R"(\binterface\s+\w+)"), - std::regex(R"(\btype\s+\w+\s*=)"), - std::regex(R"(\benum\s+\w+)"), - }; - - // Check if any TypeScript definition exists in base - bool base_has_ts_def = false; - for (const auto& line : base) { - std::string trimmed = trim(line); - for (const auto& pattern : ts_definition_patterns) { - if (std::regex_search(trimmed, pattern)) { - base_has_ts_def = true; - break; - } - } - if (base_has_ts_def) break; + const std::vector &base, + const std::vector &modified) { + // Use static regex patterns to avoid recompilation + static const std::vector ts_definition_patterns = { + std::regex(R"(\binterface\s+\w+)"), + std::regex(R"(\btype\s+\w+\s*=)"), + std::regex(R"(\benum\s+\w+)"), + }; + + // Check if any TypeScript definition exists in base + bool base_has_ts_def = false; + for (const auto &line : base) { + std::string trimmed = trim(line); + for (const auto &pattern : ts_definition_patterns) { + if (std::regex_search(trimmed, pattern)) { + base_has_ts_def = true; + break; + } } - - // Check if any TypeScript definition exists in modified - bool modified_has_ts_def = false; - for (const auto& line : modified) { - std::string trimmed = trim(line); - for (const auto& pattern : ts_definition_patterns) { - if (std::regex_search(trimmed, pattern)) { - modified_has_ts_def = true; - break; - } - } - if (modified_has_ts_def) break; + if (base_has_ts_def) + break; + } + + // Check if any TypeScript definition exists in modified + bool modified_has_ts_def = false; + for (const auto &line : modified) { + std::string trimmed = trim(line); + for (const auto &pattern : ts_definition_patterns) { + if (std::regex_search(trimmed, pattern)) { + modified_has_ts_def = true; + break; + } } - - // If either has TS definitions and content differs, it's a TS change - if (base_has_ts_def || modified_has_ts_def) { - // Check if the actual content changed - if (base.size() != modified.size()) { - return true; - } - // Cache trimmed lines to avoid repeated trim() calls - for (size_t i = 0; i < base.size(); ++i) { - std::string base_trimmed = trim(base[i]); - std::string mod_trimmed = trim(modified[i]); - if (base_trimmed != mod_trimmed) { - return true; - } - } + if (modified_has_ts_def) + break; + } + + // If either has TS definitions and content differs, it's a TS change + if (base_has_ts_def || modified_has_ts_def) { + // Check if the actual content changed + if (base.size() != modified.size()) { + return true; } - - return false; + // Cache trimmed lines to avoid repeated trim() calls + for (size_t i = 0; i < base.size(); ++i) { + std::string base_trimmed = trim(base[i]); + std::string mod_trimmed = trim(modified[i]); + if (base_trimmed != mod_trimmed) { + return true; + } + } + } + + return false; } -bool is_package_lock_file(const std::string& filename) { - // Check for package-lock.json, yarn.lock, pnpm-lock.yaml, etc. - return filename.find("package-lock.json") != std::string::npos || - filename.find("yarn.lock") != std::string::npos || - filename.find("pnpm-lock.yaml") != std::string::npos || - filename.find("bun.lockb") != std::string::npos; +bool is_package_lock_file(const std::string &filename) { + // Check for package-lock.json, yarn.lock, pnpm-lock.yaml, etc. + return filename.find("package-lock.json") != std::string::npos || + filename.find("yarn.lock") != std::string::npos || + filename.find("pnpm-lock.yaml") != std::string::npos || + filename.find("bun.lockb") != std::string::npos; } -RiskAssessment analyze_risk_ours( - const std::vector& base, - const std::vector& ours, - const std::vector& theirs -) { - RiskAssessment assessment; - assessment.level = RiskLevel::LOW; - assessment.confidence_score = 0.5; - assessment.has_syntax_changes = false; - assessment.has_logic_changes = false; - assessment.has_api_changes = false; - assessment.affects_multiple_functions = false; - assessment.affects_critical_section = false; - - // Calculate changes - size_t our_changes = count_changes(base, ours); - size_t their_changes = count_changes(base, theirs); - double similarity_to_theirs = calculate_similarity(ours, theirs); - - // Check for critical patterns - if (contains_critical_patterns(ours)) { - assessment.affects_critical_section = true; - assessment.risk_factors.push_back("Contains critical code patterns (security/data operations)"); - assessment.level = RiskLevel::HIGH; - } - - // Check for API changes - if (has_api_signature_changes(base, ours)) { - assessment.has_api_changes = true; - assessment.risk_factors.push_back("Function/method signatures changed"); - if (assessment.level < RiskLevel::MEDIUM) { - assessment.level = RiskLevel::MEDIUM; - } - } - - // Check for TypeScript interface/type changes - if (has_typescript_interface_changes(base, ours)) { - assessment.has_api_changes = true; - assessment.risk_factors.push_back("TypeScript interface or type definitions changed"); - if (assessment.level < RiskLevel::MEDIUM) { - assessment.level = RiskLevel::MEDIUM; - } - } - - // Assess based on amount of change - if (our_changes > 10) { - assessment.has_logic_changes = true; - assessment.risk_factors.push_back("Large number of changes (" + std::to_string(our_changes) + " lines)"); - if (assessment.level < RiskLevel::MEDIUM) { - assessment.level = RiskLevel::MEDIUM; - } - } - - // Check if we're discarding significant changes from theirs - if (their_changes > 5 && similarity_to_theirs < 0.3) { - assessment.risk_factors.push_back("Discarding significant changes from other branch (" + - std::to_string(their_changes) + " lines)"); - if (assessment.level < RiskLevel::MEDIUM) { - assessment.level = RiskLevel::MEDIUM; - } - } - - // Calculate confidence score based on various factors - double change_ratio = (our_changes + their_changes) > 0 ? - static_cast(our_changes) / (our_changes + their_changes) : BASE_CONFIDENCE; - assessment.confidence_score = BASE_CONFIDENCE + - (SIMILARITY_WEIGHT * similarity_to_theirs) + - (CHANGE_RATIO_WEIGHT * change_ratio); - - // Add recommendations - if (assessment.level >= RiskLevel::MEDIUM) { - assessment.recommendations.push_back("Review changes carefully before accepting"); - } - if (assessment.has_api_changes) { - assessment.recommendations.push_back("Verify API compatibility with dependent code"); - } - if (assessment.affects_critical_section) { - assessment.recommendations.push_back("Test thoroughly, especially security and data operations"); - } - if (assessment.risk_factors.empty()) { - assessment.recommendations.push_back("Changes appear safe to accept"); - } - - return assessment; -} +RiskAssessment analyze_risk_ours(const std::vector &base, + const std::vector &ours, + const std::vector &theirs) { + RiskAssessment assessment; + assessment.level = RiskLevel::LOW; + assessment.confidence_score = 0.5; + assessment.has_syntax_changes = false; + assessment.has_logic_changes = false; + assessment.has_api_changes = false; + assessment.affects_multiple_functions = false; + assessment.affects_critical_section = false; -RiskAssessment analyze_risk_theirs( - const std::vector& base, - const std::vector& ours, - const std::vector& theirs -) { - RiskAssessment assessment; - assessment.level = RiskLevel::LOW; - assessment.confidence_score = 0.5; - assessment.has_syntax_changes = false; - assessment.has_logic_changes = false; - assessment.has_api_changes = false; - assessment.affects_multiple_functions = false; - assessment.affects_critical_section = false; - - // Calculate changes - size_t our_changes = count_changes(base, ours); - size_t their_changes = count_changes(base, theirs); - double similarity_to_ours = calculate_similarity(theirs, ours); - - // Check for critical patterns - if (contains_critical_patterns(theirs)) { - assessment.affects_critical_section = true; - assessment.risk_factors.push_back("Contains critical code patterns (security/data operations)"); - assessment.level = RiskLevel::HIGH; - } - - // Check for API changes - if (has_api_signature_changes(base, theirs)) { - assessment.has_api_changes = true; - assessment.risk_factors.push_back("Function/method signatures changed"); - if (assessment.level < RiskLevel::MEDIUM) { - assessment.level = RiskLevel::MEDIUM; - } - } - - // Check for TypeScript interface/type changes - if (has_typescript_interface_changes(base, theirs)) { - assessment.has_api_changes = true; - assessment.risk_factors.push_back("TypeScript interface or type definitions changed"); - if (assessment.level < RiskLevel::MEDIUM) { - assessment.level = RiskLevel::MEDIUM; - } - } - - // Assess based on amount of change - if (their_changes > 10) { - assessment.has_logic_changes = true; - assessment.risk_factors.push_back("Large number of changes (" + std::to_string(their_changes) + " lines)"); - if (assessment.level < RiskLevel::MEDIUM) { - assessment.level = RiskLevel::MEDIUM; - } - } - - // Check if we're discarding our changes - if (our_changes > 5 && similarity_to_ours < 0.3) { - assessment.risk_factors.push_back("Discarding our local changes (" + - std::to_string(our_changes) + " lines)"); - if (assessment.level < RiskLevel::MEDIUM) { - assessment.level = RiskLevel::MEDIUM; - } - } - - // Calculate confidence score - double change_ratio = (our_changes + their_changes) > 0 ? - static_cast(their_changes) / (our_changes + their_changes) : BASE_CONFIDENCE; - assessment.confidence_score = BASE_CONFIDENCE + - (SIMILARITY_WEIGHT * similarity_to_ours) + - (CHANGE_RATIO_WEIGHT * change_ratio); - - // Add recommendations - if (assessment.level >= RiskLevel::MEDIUM) { - assessment.recommendations.push_back("Review changes carefully before accepting"); - } - if (assessment.has_api_changes) { - assessment.recommendations.push_back("Verify API compatibility with dependent code"); - } - if (assessment.affects_critical_section) { - assessment.recommendations.push_back("Test thoroughly, especially security and data operations"); - } - if (assessment.risk_factors.empty()) { - assessment.recommendations.push_back("Changes appear safe to accept"); - } - - return assessment; -} + // Calculate changes + size_t our_changes = count_changes(base, ours); + size_t their_changes = count_changes(base, theirs); + double similarity_to_theirs = calculate_similarity(ours, theirs); -RiskAssessment analyze_risk_both( - const std::vector& base, - const std::vector& ours, - const std::vector& theirs -) { - RiskAssessment assessment; - assessment.level = RiskLevel::MEDIUM; // Default to medium for concatenation - assessment.confidence_score = 0.3; // Lower confidence for concatenation - assessment.has_syntax_changes = true; + // Check for critical patterns + if (contains_critical_patterns(ours)) { + assessment.affects_critical_section = true; + assessment.risk_factors.push_back( + "Contains critical code patterns (security/data operations)"); + assessment.level = RiskLevel::HIGH; + } + + // Check for API changes + if (has_api_signature_changes(base, ours)) { + assessment.has_api_changes = true; + assessment.risk_factors.push_back("Function/method signatures changed"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Check for TypeScript interface/type changes + if (has_typescript_interface_changes(base, ours)) { + assessment.has_api_changes = true; + assessment.risk_factors.push_back( + "TypeScript interface or type definitions changed"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Assess based on amount of change + if (our_changes > 10) { assessment.has_logic_changes = true; - assessment.has_api_changes = false; - assessment.affects_multiple_functions = false; - assessment.affects_critical_section = false; - - // Concatenating both versions is generally risky - assessment.risk_factors.push_back("Concatenating both versions may cause duplicates or conflicts"); - - // Check if either contains critical patterns - if (contains_critical_patterns(ours) || contains_critical_patterns(theirs)) { - assessment.affects_critical_section = true; - assessment.risk_factors.push_back("Contains critical code patterns that may conflict"); - assessment.level = RiskLevel::HIGH; + assessment.risk_factors.push_back("Large number of changes (" + + std::to_string(our_changes) + " lines)"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; } - - // Check for duplicate logic - double similarity = calculate_similarity(ours, theirs); - if (similarity > 0.5) { - assessment.risk_factors.push_back("High similarity may result in duplicate code"); - assessment.level = RiskLevel::HIGH; + } + + // Check if we're discarding significant changes from theirs + if (their_changes > 5 && similarity_to_theirs < 0.3) { + assessment.risk_factors.push_back( + "Discarding significant changes from other branch (" + + std::to_string(their_changes) + " lines)"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; } - - // API changes from either side - if (has_api_signature_changes(base, ours) || has_api_signature_changes(base, theirs)) { - assessment.has_api_changes = true; - assessment.risk_factors.push_back("Multiple API changes may cause conflicts"); - assessment.level = RiskLevel::HIGH; - } - - // TypeScript interface/type changes from either side - if (has_typescript_interface_changes(base, ours) || has_typescript_interface_changes(base, theirs)) { - assessment.has_api_changes = true; - assessment.risk_factors.push_back("Multiple TypeScript interface/type changes may cause conflicts"); - assessment.level = RiskLevel::HIGH; - } - - // Recommendations for concatenation - assessment.recommendations.push_back("Manual review required - automatic concatenation is risky"); - assessment.recommendations.push_back("Consider merging logic manually instead of concatenating"); - assessment.recommendations.push_back("Test thoroughly for duplicate or conflicting code"); - - return assessment; + } + + // Calculate confidence score based on various factors + double change_ratio = + (our_changes + their_changes) > 0 + ? static_cast(our_changes) / (our_changes + their_changes) + : BASE_CONFIDENCE; + assessment.confidence_score = BASE_CONFIDENCE + + (SIMILARITY_WEIGHT * similarity_to_theirs) + + (CHANGE_RATIO_WEIGHT * change_ratio); + + // Add recommendations + if (assessment.level >= RiskLevel::MEDIUM) { + assessment.recommendations.push_back( + "Review changes carefully before accepting"); + } + if (assessment.has_api_changes) { + assessment.recommendations.push_back( + "Verify API compatibility with dependent code"); + } + if (assessment.affects_critical_section) { + assessment.recommendations.push_back( + "Test thoroughly, especially security and data operations"); + } + if (assessment.risk_factors.empty()) { + assessment.recommendations.push_back("Changes appear safe to accept"); + } + + return assessment; } -} // namespace analysis -} // namespace wizardmerge +RiskAssessment analyze_risk_theirs(const std::vector &base, + const std::vector &ours, + const std::vector &theirs) { + RiskAssessment assessment; + assessment.level = RiskLevel::LOW; + assessment.confidence_score = 0.5; + assessment.has_syntax_changes = false; + assessment.has_logic_changes = false; + assessment.has_api_changes = false; + assessment.affects_multiple_functions = false; + assessment.affects_critical_section = false; + + // Calculate changes + size_t our_changes = count_changes(base, ours); + size_t their_changes = count_changes(base, theirs); + double similarity_to_ours = calculate_similarity(theirs, ours); + + // Check for critical patterns + if (contains_critical_patterns(theirs)) { + assessment.affects_critical_section = true; + assessment.risk_factors.push_back( + "Contains critical code patterns (security/data operations)"); + assessment.level = RiskLevel::HIGH; + } + + // Check for API changes + if (has_api_signature_changes(base, theirs)) { + assessment.has_api_changes = true; + assessment.risk_factors.push_back("Function/method signatures changed"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Check for TypeScript interface/type changes + if (has_typescript_interface_changes(base, theirs)) { + assessment.has_api_changes = true; + assessment.risk_factors.push_back( + "TypeScript interface or type definitions changed"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Assess based on amount of change + if (their_changes > 10) { + assessment.has_logic_changes = true; + assessment.risk_factors.push_back("Large number of changes (" + + std::to_string(their_changes) + + " lines)"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Check if we're discarding our changes + if (our_changes > 5 && similarity_to_ours < 0.3) { + assessment.risk_factors.push_back("Discarding our local changes (" + + std::to_string(our_changes) + " lines)"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Calculate confidence score + double change_ratio = + (our_changes + their_changes) > 0 + ? static_cast(their_changes) / (our_changes + their_changes) + : BASE_CONFIDENCE; + assessment.confidence_score = BASE_CONFIDENCE + + (SIMILARITY_WEIGHT * similarity_to_ours) + + (CHANGE_RATIO_WEIGHT * change_ratio); + + // Add recommendations + if (assessment.level >= RiskLevel::MEDIUM) { + assessment.recommendations.push_back( + "Review changes carefully before accepting"); + } + if (assessment.has_api_changes) { + assessment.recommendations.push_back( + "Verify API compatibility with dependent code"); + } + if (assessment.affects_critical_section) { + assessment.recommendations.push_back( + "Test thoroughly, especially security and data operations"); + } + if (assessment.risk_factors.empty()) { + assessment.recommendations.push_back("Changes appear safe to accept"); + } + + return assessment; +} + +RiskAssessment analyze_risk_both(const std::vector &base, + const std::vector &ours, + const std::vector &theirs) { + RiskAssessment assessment; + assessment.level = RiskLevel::MEDIUM; // Default to medium for concatenation + assessment.confidence_score = 0.3; // Lower confidence for concatenation + assessment.has_syntax_changes = true; + assessment.has_logic_changes = true; + assessment.has_api_changes = false; + assessment.affects_multiple_functions = false; + assessment.affects_critical_section = false; + + // Concatenating both versions is generally risky + assessment.risk_factors.push_back( + "Concatenating both versions may cause duplicates or conflicts"); + + // Check if either contains critical patterns + if (contains_critical_patterns(ours) || contains_critical_patterns(theirs)) { + assessment.affects_critical_section = true; + assessment.risk_factors.push_back( + "Contains critical code patterns that may conflict"); + assessment.level = RiskLevel::HIGH; + } + + // Check for duplicate logic + double similarity = calculate_similarity(ours, theirs); + if (similarity > 0.5) { + assessment.risk_factors.push_back( + "High similarity may result in duplicate code"); + assessment.level = RiskLevel::HIGH; + } + + // API changes from either side + if (has_api_signature_changes(base, ours) || + has_api_signature_changes(base, theirs)) { + assessment.has_api_changes = true; + assessment.risk_factors.push_back( + "Multiple API changes may cause conflicts"); + assessment.level = RiskLevel::HIGH; + } + + // TypeScript interface/type changes from either side + if (has_typescript_interface_changes(base, ours) || + has_typescript_interface_changes(base, theirs)) { + assessment.has_api_changes = true; + assessment.risk_factors.push_back( + "Multiple TypeScript interface/type changes may cause conflicts"); + assessment.level = RiskLevel::HIGH; + } + + // Recommendations for concatenation + assessment.recommendations.push_back( + "Manual review required - automatic concatenation is risky"); + assessment.recommendations.push_back( + "Consider merging logic manually instead of concatenating"); + assessment.recommendations.push_back( + "Test thoroughly for duplicate or conflicting code"); + + return assessment; +} + +} // namespace analysis +} // namespace wizardmerge diff --git a/backend/src/controllers/MergeController.h b/backend/src/controllers/MergeController.h index 4d25cd3..be2621f 100644 --- a/backend/src/controllers/MergeController.h +++ b/backend/src/controllers/MergeController.h @@ -17,33 +17,33 @@ namespace controllers { * @brief HTTP controller for three-way merge API */ class MergeController : public HttpController { - public: - METHOD_LIST_BEGIN - // POST /api/merge - Perform three-way merge - ADD_METHOD_TO(MergeController::merge, "/api/merge", Post); - METHOD_LIST_END +public: + METHOD_LIST_BEGIN + // POST /api/merge - Perform three-way merge + ADD_METHOD_TO(MergeController::merge, "/api/merge", Post); + METHOD_LIST_END - /** - * @brief Perform three-way merge operation - * - * Request body should be JSON: - * { - * "base": ["line1", "line2", ...], - * "ours": ["line1", "line2", ...], - * "theirs": ["line1", "line2", ...] - * } - * - * Response: - * { - * "merged": ["line1", "line2", ...], - * "conflicts": [...] - * } - */ - void merge(const HttpRequestPtr &req, - std::function &&callback); + /** + * @brief Perform three-way merge operation + * + * Request body should be JSON: + * { + * "base": ["line1", "line2", ...], + * "ours": ["line1", "line2", ...], + * "theirs": ["line1", "line2", ...] + * } + * + * Response: + * { + * "merged": ["line1", "line2", ...], + * "conflicts": [...] + * } + */ + void merge(const HttpRequestPtr &req, + std::function &&callback); }; -} // namespace controllers -} // namespace wizardmerge +} // namespace controllers +} // namespace wizardmerge -#endif // WIZARDMERGE_CONTROLLERS_MERGE_CONTROLLER_H +#endif // WIZARDMERGE_CONTROLLERS_MERGE_CONTROLLER_H diff --git a/backend/src/controllers/PRController.h b/backend/src/controllers/PRController.h index 0cffdbb..000167e 100644 --- a/backend/src/controllers/PRController.h +++ b/backend/src/controllers/PRController.h @@ -17,49 +17,49 @@ namespace controllers { * @brief HTTP controller for pull request merge API */ class PRController : public HttpController { - 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 +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 &&callback); + /** + * @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 &&callback); }; -} // namespace controllers -} // namespace wizardmerge +} // namespace controllers +} // namespace wizardmerge -#endif // WIZARDMERGE_CONTROLLERS_PR_CONTROLLER_H +#endif // WIZARDMERGE_CONTROLLERS_PR_CONTROLLER_H diff --git a/backend/src/git/git_cli.cpp b/backend/src/git/git_cli.cpp index d0f7d6f..bd21c54 100644 --- a/backend/src/git/git_cli.cpp +++ b/backend/src/git/git_cli.cpp @@ -4,11 +4,11 @@ */ #include "wizardmerge/git/git_cli.h" -#include #include -#include -#include +#include #include +#include +#include #include namespace wizardmerge { @@ -19,222 +19,209 @@ 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 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; - } - +GitResult execute_command(const std::string &command) { + GitResult result; + result.exit_code = 0; + + // Execute command and capture output + std::array 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; +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 +} // anonymous namespace bool is_git_available() { - GitResult result = execute_command("git --version"); - return result.success; + 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 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 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 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& 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 add_files(const std::string &repo_path, + const std::vector &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()) { - auto name_result = execute_command(git_command(repo_path, - "config user.name \"" + config.user_name + "\"")); - if (!name_result.success) { - GitResult result; - result.success = false; - result.error = "Failed to set user.name: " + name_result.error; - result.exit_code = name_result.exit_code; - return result; - } - - auto email_result = execute_command(git_command(repo_path, - "config user.email \"" + config.user_email + "\"")); - if (!email_result.success) { - GitResult result; - result.success = false; - result.error = "Failed to set user.email: " + email_result.error; - result.exit_code = email_result.exit_code; - return result; - } +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()) { + auto name_result = execute_command(git_command( + repo_path, "config user.name \"" + config.user_name + "\"")); + if (!name_result.success) { + GitResult result; + result.success = false; + result.error = "Failed to set user.name: " + name_result.error; + result.exit_code = name_result.exit_code; + return result; } - - // Escape commit message for shell - std::string escaped_message = message; - size_t pos = 0; - while ((pos = escaped_message.find("\"", pos)) != std::string::npos) { - escaped_message.replace(pos, 1, "\\\""); - pos += 2; + + auto email_result = execute_command(git_command( + repo_path, "config user.email \"" + config.user_email + "\"")); + if (!email_result.success) { + GitResult result; + result.success = false; + result.error = "Failed to set user.email: " + email_result.error; + result.exit_code = email_result.exit_code; + return result; } - - std::string cmd = "commit -m \"" + escaped_message + "\""; - return execute_command(git_command(repo_path, cmd)); + } + + // Escape commit message for shell + std::string escaped_message = message; + size_t pos = 0; + while ((pos = escaped_message.find("\"", pos)) != std::string::npos) { + escaped_message.replace(pos, 1, "\\\""); + pos += 2; + } + + std::string cmd = "commit -m \"" + escaped_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); +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 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; - size_t last_non_ws = branch.find_last_not_of(" \n\r\t"); - - if (last_non_ws == std::string::npos) { - // String contains only whitespace - return std::nullopt; - } - - branch.erase(last_non_ws + 1); - - return branch; +std::optional 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; + size_t last_non_ws = branch.find_last_not_of(" \n\r\t"); + + if (last_non_ws == std::string::npos) { + // String contains only whitespace + return std::nullopt; + } + + branch.erase(last_non_ws + 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; +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")); +GitResult status(const std::string &repo_path) { + return execute_command(git_command(repo_path, "status")); } -} // namespace git -} // namespace wizardmerge +} // namespace git +} // namespace wizardmerge diff --git a/backend/src/git/git_platform_client.cpp b/backend/src/git/git_platform_client.cpp index 4a9aa76..7d77527 100644 --- a/backend/src/git/git_platform_client.cpp +++ b/backend/src/git/git_platform_client.cpp @@ -4,12 +4,12 @@ */ #include "wizardmerge/git/git_platform_client.h" -#include -#include -#include #include #include +#include #include +#include +#include namespace wizardmerge { namespace git { @@ -19,399 +19,417 @@ 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; - } +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; + } + return decoded; } // 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; +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, GitPlatform platform = GitPlatform::GitHub) { - CURL* curl = curl_easy_init(); - if (!curl) { - std::cerr << "Failed to initialize CURL" << std::endl; - return false; +bool http_get(const std::string &url, const std::string &token, + std::string &response, + GitPlatform platform = GitPlatform::GitHub) { + 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 based on platform + struct curl_slist *headers = nullptr; + + if (platform == GitPlatform::GitHub) { + 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()); } - - 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 based on platform - struct curl_slist* headers = nullptr; - - if (platform == GitPlatform::GitHub) { - 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()); - } - } else if (platform == GitPlatform::GitLab) { - headers = curl_slist_append(headers, "Accept: application/json"); - if (!token.empty()) { - std::string auth_header = "PRIVATE-TOKEN: " + token; - headers = curl_slist_append(headers, auth_header.c_str()); - } + } else if (platform == GitPlatform::GitLab) { + headers = curl_slist_append(headers, "Accept: application/json"); + if (!token.empty()) { + std::string auth_header = "PRIVATE-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_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_slist_free_all(headers); - curl_easy_cleanup(curl); + CURLcode res = curl_easy_perform(curl); - return success; + 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 split_lines(const std::string& content) { - std::vector lines; - std::istringstream stream(content); - std::string line; - - while (std::getline(stream, line)) { - lines.push_back(line); - } - - return lines; +std::vector split_lines(const std::string &content) { + std::vector lines; + std::istringstream stream(content); + std::string line; + + while (std::getline(stream, line)) { + lines.push_back(line); + } + + return lines; } -} // anonymous namespace +} // anonymous namespace -bool parse_pr_url(const std::string& url, GitPlatform& platform, - std::string& owner, std::string& repo, int& pr_number) { - // Try GitHub pattern first: - // https://github.com/owner/repo/pull/123 - // github.com/owner/repo/pull/123 - - std::regex github_regex(R"((?:https?://)?(?:www\.)?github\.com/([^/]+)/([^/]+)/pull/(\d+))"); - std::smatch matches; - - if (std::regex_search(url, matches, github_regex)) { - if (matches.size() == 4) { - platform = GitPlatform::GitHub; - owner = matches[1].str(); - repo = matches[2].str(); - pr_number = std::stoi(matches[3].str()); - return true; - } +bool parse_pr_url(const std::string &url, GitPlatform &platform, + std::string &owner, std::string &repo, int &pr_number) { + // Try GitHub pattern first: + // https://github.com/owner/repo/pull/123 + // github.com/owner/repo/pull/123 + + std::regex github_regex( + R"((?:https?://)?(?:www\.)?github\.com/([^/]+)/([^/]+)/pull/(\d+))"); + std::smatch matches; + + if (std::regex_search(url, matches, github_regex)) { + if (matches.size() == 4) { + platform = GitPlatform::GitHub; + owner = matches[1].str(); + repo = matches[2].str(); + pr_number = std::stoi(matches[3].str()); + return true; } - - // Try GitLab pattern: - // https://gitlab.com/owner/repo/-/merge_requests/456 - // gitlab.com/group/subgroup/project/-/merge_requests/789 - - std::regex gitlab_regex(R"((?:https?://)?(?:www\.)?gitlab\.com/([^/-]+(?:/[^/-]+)*?)/-/merge_requests/(\d+))"); - - if (std::regex_search(url, matches, gitlab_regex)) { - if (matches.size() == 3) { - platform = GitPlatform::GitLab; - std::string full_path = matches[1].str(); - - // For GitLab, store the full project path - // The path can be: owner/repo or group/subgroup/project - // We split at the last slash to separate for potential use - size_t last_slash = full_path.find_last_of('/'); - if (last_slash != std::string::npos) { - owner = full_path.substr(0, last_slash); - repo = full_path.substr(last_slash + 1); - } else { - // Single level project (rare but possible) - // Store entire path as owner, repo empty - // API calls will use full path by concatenating - owner = full_path; - repo = ""; - } - - pr_number = std::stoi(matches[2].str()); - return true; - } + } + + // Try GitLab pattern: + // https://gitlab.com/owner/repo/-/merge_requests/456 + // gitlab.com/group/subgroup/project/-/merge_requests/789 + + std::regex gitlab_regex( + R"((?:https?://)?(?:www\.)?gitlab\.com/([^/-]+(?:/[^/-]+)*?)/-/merge_requests/(\d+))"); + + if (std::regex_search(url, matches, gitlab_regex)) { + if (matches.size() == 3) { + platform = GitPlatform::GitLab; + std::string full_path = matches[1].str(); + + // For GitLab, store the full project path + // The path can be: owner/repo or group/subgroup/project + // We split at the last slash to separate for potential use + size_t last_slash = full_path.find_last_of('/'); + if (last_slash != std::string::npos) { + owner = full_path.substr(0, last_slash); + repo = full_path.substr(last_slash + 1); + } else { + // Single level project (rare but possible) + // Store entire path as owner, repo empty + // API calls will use full path by concatenating + owner = full_path; + repo = ""; + } + + pr_number = std::stoi(matches[2].str()); + return true; } - - platform = GitPlatform::Unknown; - return false; + } + + platform = GitPlatform::Unknown; + return false; } -std::optional fetch_pull_request( - GitPlatform platform, - const std::string& owner, - const std::string& repo, - int pr_number, - const std::string& token -) { - PullRequest pr; - pr.platform = platform; - pr.number = pr_number; - pr.repo_owner = owner; - pr.repo_name = repo; - - std::string pr_url, files_url; - - if (platform == GitPlatform::GitHub) { - // GitHub API URLs - pr_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + std::to_string(pr_number); - files_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + std::to_string(pr_number) + "/files"; - } else if (platform == GitPlatform::GitLab) { - // GitLab API URLs - encode project path - std::string project_path = owner; - if (!repo.empty()) { - project_path += "/" + repo; - } - // URL encode the project path - CURL* curl = curl_easy_init(); - char* encoded = curl_easy_escape(curl, project_path.c_str(), project_path.length()); - std::string encoded_project = encoded; - curl_free(encoded); - curl_easy_cleanup(curl); - - pr_url = "https://gitlab.com/api/v4/projects/" + encoded_project + "/merge_requests/" + std::to_string(pr_number); - files_url = "https://gitlab.com/api/v4/projects/" + encoded_project + "/merge_requests/" + std::to_string(pr_number) + "/changes"; - } else { - std::cerr << "Unknown platform" << std::endl; - return std::nullopt; +std::optional fetch_pull_request(GitPlatform platform, + const std::string &owner, + const std::string &repo, + int pr_number, + const std::string &token) { + PullRequest pr; + pr.platform = platform; + pr.number = pr_number; + pr.repo_owner = owner; + pr.repo_name = repo; + + std::string pr_url, files_url; + + if (platform == GitPlatform::GitHub) { + // GitHub API URLs + pr_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + + std::to_string(pr_number); + files_url = "https://api.github.com/repos/" + owner + "/" + repo + + "/pulls/" + std::to_string(pr_number) + "/files"; + } else if (platform == GitPlatform::GitLab) { + // GitLab API URLs - encode project path + std::string project_path = owner; + if (!repo.empty()) { + project_path += "/" + repo; } - - // Fetch PR/MR info - std::string response; - if (!http_get(pr_url, token, response, platform)) { - std::cerr << "Failed to fetch pull/merge request info" << std::endl; - return std::nullopt; + // URL encode the project path + CURL *curl = curl_easy_init(); + char *encoded = + curl_easy_escape(curl, project_path.c_str(), project_path.length()); + std::string encoded_project = encoded; + curl_free(encoded); + curl_easy_cleanup(curl); + + pr_url = "https://gitlab.com/api/v4/projects/" + encoded_project + + "/merge_requests/" + std::to_string(pr_number); + files_url = "https://gitlab.com/api/v4/projects/" + encoded_project + + "/merge_requests/" + std::to_string(pr_number) + "/changes"; + } else { + std::cerr << "Unknown platform" << std::endl; + return std::nullopt; + } + + // Fetch PR/MR info + std::string response; + if (!http_get(pr_url, token, response, platform)) { + std::cerr << "Failed to fetch pull/merge 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/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 (root.isMember("base") && root["base"].isObject()) { + pr.base_ref = root["base"].get("ref", "").asString(); + pr.base_sha = root["base"].get("sha", "").asString(); } - // Parse JSON response + 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(); + } 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.mergeable = (merge_status == "can_be_merged"); + pr.mergeable_state = merge_status; + } + + // Fetch PR/MR files + std::string files_response; + + if (!http_get(files_url, token, files_response, platform)) { + std::cerr << "Failed to fetch pull/merge 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; + } + + // 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); + } + } + } +} + +return pr; +} + +std::optional> +fetch_file_content(GitPlatform platform, const std::string &owner, + const std::string &repo, const std::string &sha, + const std::string &path, const std::string &token) { + std::string url; + + if (platform == GitPlatform::GitHub) { + // GitHub API URL + url = "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" + + path + "?ref=" + sha; + } else if (platform == GitPlatform::GitLab) { + // GitLab API URL - encode project path and file path + std::string project_path = owner; + if (!repo.empty()) { + project_path += "/" + repo; + } + + CURL *curl = curl_easy_init(); + char *encoded_project = + curl_easy_escape(curl, project_path.c_str(), project_path.length()); + char *encoded_path = curl_easy_escape(curl, path.c_str(), path.length()); + + url = "https://gitlab.com/api/v4/projects/" + std::string(encoded_project) + + "/repository/files/" + std::string(encoded_path) + "/raw?ref=" + sha; + + curl_free(encoded_project); + curl_free(encoded_path); + curl_easy_cleanup(curl); + } else { + std::cerr << "Unknown platform" << std::endl; + return std::nullopt; + } + + std::string response; + + if (!http_get(url, token, response, platform)) { + std::cerr << "Failed to fetch file content for " << path << " at " << sha + << std::endl; + return std::nullopt; + } + + // 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 PR/MR JSON: " << errs << std::endl; - return std::nullopt; + std::cerr << "Failed to parse content JSON: " << errs << std::endl; + return std::nullopt; } - 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 (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(); - } 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.mergeable = (merge_status == "can_be_merged"); - pr.mergeable_state = merge_status; + // 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; } - // Fetch PR/MR files - std::string files_response; - - if (!http_get(files_url, token, files_response, platform)) { - std::cerr << "Failed to fetch pull/merge request files" << std::endl; - return std::nullopt; + std::string encoding = root["encoding"].asString(); + if (encoding != "base64") { + std::cerr << "Unsupported encoding: " << encoding << 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; + // 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; } - // 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); - } - } - } - } + // Split content into lines + return split_lines(decoded_content); + } else if (platform == GitPlatform::GitLab) { + // GitLab returns raw file content directly + return split_lines(response); + } - return pr; + return std::nullopt; } -std::optional> fetch_file_content( - GitPlatform platform, - const std::string& owner, - const std::string& repo, - const std::string& sha, - const std::string& path, - const std::string& token -) { - std::string url; - - if (platform == GitPlatform::GitHub) { - // GitHub API URL - url = "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" + path + "?ref=" + sha; - } else if (platform == GitPlatform::GitLab) { - // GitLab API URL - encode project path and file path - std::string project_path = owner; - if (!repo.empty()) { - project_path += "/" + repo; - } - - CURL* curl = curl_easy_init(); - char* encoded_project = curl_easy_escape(curl, project_path.c_str(), project_path.length()); - char* encoded_path = curl_easy_escape(curl, path.c_str(), path.length()); - - url = "https://gitlab.com/api/v4/projects/" + std::string(encoded_project) + - "/repository/files/" + std::string(encoded_path) + "/raw?ref=" + sha; - - curl_free(encoded_project); - curl_free(encoded_path); - curl_easy_cleanup(curl); - } else { - std::cerr << "Unknown platform" << std::endl; - return std::nullopt; - } - - std::string response; - - if (!http_get(url, token, response, platform)) { - std::cerr << "Failed to fetch file content for " << path << " at " << sha << std::endl; - return std::nullopt; - } - - // 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; - } - - // 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()); - - // 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) { - // GitLab returns raw file content directly - return split_lines(response); - } - - return std::nullopt; -} - -} // namespace git -} // namespace wizardmerge +} // namespace git +} // namespace wizardmerge diff --git a/backend/src/main.cpp b/backend/src/main.cpp index 3652ed9..0627957 100644 --- a/backend/src/main.cpp +++ b/backend/src/main.cpp @@ -3,52 +3,52 @@ * @brief HTTP API server for WizardMerge using Drogon framework */ -#include -#include #include "controllers/MergeController.h" +#include +#include using namespace drogon; -int main(int argc, char* argv[]) { - std::cout << "WizardMerge - Intelligent Merge Conflict Resolution API\n"; - std::cout << "======================================================\n"; - std::cout << "Starting HTTP server...\n\n"; - - // Load configuration from file - std::string config_file = "config.json"; - if (argc > 1) { - config_file = argv[1]; +int main(int argc, char *argv[]) { + std::cout << "WizardMerge - Intelligent Merge Conflict Resolution API\n"; + std::cout << "======================================================\n"; + std::cout << "Starting HTTP server...\n\n"; + + // Load configuration from file + std::string config_file = "config.json"; + if (argc > 1) { + config_file = argv[1]; + } + + try { + // Load configuration and start server + app().loadConfigFile(config_file); + + // Display listener information if available + auto listeners = app().getListeners(); + if (!listeners.empty()) { + try { + std::cout << "Server will listen on port " << listeners[0].toPort + << "\n"; + } catch (...) { + std::cout << "Server listener configured\n"; + } + } else { + std::cout << "Server configuration loaded\n"; } - - try { - // Load configuration and start server - app().loadConfigFile(config_file); - - // Display listener information if available - auto listeners = app().getListeners(); - if (!listeners.empty()) { - try { - std::cout << "Server will listen on port " - << listeners[0].toPort << "\n"; - } catch (...) { - std::cout << "Server listener configured\n"; - } - } else { - std::cout << "Server configuration loaded\n"; - } - - std::cout << "Available endpoints:\n"; - std::cout << " POST /api/merge - Three-way merge API\n"; - std::cout << "\nPress Ctrl+C to stop the server.\n\n"; - - // Run the application - app().run(); - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << '\n'; - std::cerr << "Failed to load config file: " << config_file << '\n'; - std::cerr << "Usage: " << argv[0] << " [config.json]\n"; - return 1; - } - - return 0; + + std::cout << "Available endpoints:\n"; + std::cout << " POST /api/merge - Three-way merge API\n"; + std::cout << "\nPress Ctrl+C to stop the server.\n\n"; + + // Run the application + app().run(); + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << '\n'; + std::cerr << "Failed to load config file: " << config_file << '\n'; + std::cerr << "Usage: " << argv[0] << " [config.json]\n"; + return 1; + } + + return 0; } diff --git a/backend/src/merge/three_way_merge.cpp b/backend/src/merge/three_way_merge.cpp index 7a58132..ff5d220 100644 --- a/backend/src/merge/three_way_merge.cpp +++ b/backend/src/merge/three_way_merge.cpp @@ -16,117 +16,118 @@ namespace { /** * @brief Check if two lines are effectively equal (ignoring whitespace). */ -bool lines_equal_ignore_whitespace(const std::string& a, const std::string& b) { - auto trim = [](const std::string& s) { - size_t start = s.find_first_not_of(" \t\n\r"); - size_t end = s.find_last_not_of(" \t\n\r"); - if (start == std::string::npos) return std::string(); - return s.substr(start, end - start + 1); - }; - return trim(a) == trim(b); +bool lines_equal_ignore_whitespace(const std::string &a, const std::string &b) { + auto trim = [](const std::string &s) { + size_t start = s.find_first_not_of(" \t\n\r"); + size_t end = s.find_last_not_of(" \t\n\r"); + if (start == std::string::npos) + return std::string(); + return s.substr(start, end - start + 1); + }; + return trim(a) == trim(b); } -} // namespace +} // namespace -MergeResult three_way_merge( - const std::vector& base, - const std::vector& ours, - const std::vector& theirs -) { - MergeResult result; - - // Simple line-by-line comparison for initial implementation - // This is a placeholder - full algorithm will use dependency analysis - - size_t max_len = std::max({base.size(), ours.size(), theirs.size()}); - - for (size_t i = 0; i < max_len; ++i) { - std::string base_line = (i < base.size()) ? base[i] : ""; - std::string our_line = (i < ours.size()) ? ours[i] : ""; - std::string their_line = (i < theirs.size()) ? theirs[i] : ""; - - // Case 1: All three are the same - use as-is - if (base_line == our_line && base_line == their_line) { - result.merged_lines.push_back({base_line, Line::BASE}); - } - // Case 2: Base == Ours, but Theirs changed - use theirs - else if (base_line == our_line && base_line != their_line) { - result.merged_lines.push_back({their_line, Line::THEIRS}); - } - // Case 3: Base == Theirs, but Ours changed - use ours - else if (base_line == their_line && base_line != our_line) { - result.merged_lines.push_back({our_line, Line::OURS}); - } - // Case 4: Ours == Theirs, but different from Base - use the common change - else if (our_line == their_line && our_line != base_line) { - result.merged_lines.push_back({our_line, Line::MERGED}); - } - // Case 5: All different - conflict - else { - Conflict conflict; - conflict.start_line = result.merged_lines.size(); - conflict.base_lines.push_back({base_line, Line::BASE}); - conflict.our_lines.push_back({our_line, Line::OURS}); - conflict.their_lines.push_back({their_line, Line::THEIRS}); - conflict.end_line = result.merged_lines.size(); - - // Perform context analysis using ours version as context - // (could also use base or theirs, but ours is typically most relevant) - conflict.context = analysis::analyze_context(ours, i, i); - - // Perform risk analysis for different resolution strategies - std::vector base_vec = {base_line}; - std::vector ours_vec = {our_line}; - std::vector theirs_vec = {their_line}; - - conflict.risk_ours = analysis::analyze_risk_ours(base_vec, ours_vec, theirs_vec); - conflict.risk_theirs = analysis::analyze_risk_theirs(base_vec, ours_vec, theirs_vec); - conflict.risk_both = analysis::analyze_risk_both(base_vec, ours_vec, theirs_vec); - - result.conflicts.push_back(conflict); - - // Add conflict markers - result.merged_lines.push_back({"<<<<<<< OURS", Line::MERGED}); - result.merged_lines.push_back({our_line, Line::OURS}); - result.merged_lines.push_back({"=======", Line::MERGED}); - result.merged_lines.push_back({their_line, Line::THEIRS}); - result.merged_lines.push_back({">>>>>>> THEIRS", Line::MERGED}); - } +MergeResult three_way_merge(const std::vector &base, + const std::vector &ours, + const std::vector &theirs) { + MergeResult result; + + // Simple line-by-line comparison for initial implementation + // This is a placeholder - full algorithm will use dependency analysis + + size_t max_len = std::max({base.size(), ours.size(), theirs.size()}); + + for (size_t i = 0; i < max_len; ++i) { + std::string base_line = (i < base.size()) ? base[i] : ""; + std::string our_line = (i < ours.size()) ? ours[i] : ""; + std::string their_line = (i < theirs.size()) ? theirs[i] : ""; + + // Case 1: All three are the same - use as-is + if (base_line == our_line && base_line == their_line) { + result.merged_lines.push_back({base_line, Line::BASE}); } - - return result; -} - -MergeResult auto_resolve(const MergeResult& result) { - MergeResult resolved = result; - - // Auto-resolve whitespace-only differences - std::vector remaining_conflicts; - - for (const auto& conflict : result.conflicts) { - bool can_resolve = false; - - // Check if differences are whitespace-only - if (conflict.our_lines.size() == conflict.their_lines.size()) { - can_resolve = true; - for (size_t i = 0; i < conflict.our_lines.size(); ++i) { - if (!lines_equal_ignore_whitespace( - conflict.our_lines[i].content, - conflict.their_lines[i].content)) { - can_resolve = false; - break; - } - } - } - - if (!can_resolve) { - remaining_conflicts.push_back(conflict); - } + // Case 2: Base == Ours, but Theirs changed - use theirs + else if (base_line == our_line && base_line != their_line) { + result.merged_lines.push_back({their_line, Line::THEIRS}); } - - resolved.conflicts = remaining_conflicts; - return resolved; + // Case 3: Base == Theirs, but Ours changed - use ours + else if (base_line == their_line && base_line != our_line) { + result.merged_lines.push_back({our_line, Line::OURS}); + } + // Case 4: Ours == Theirs, but different from Base - use the common change + else if (our_line == their_line && our_line != base_line) { + result.merged_lines.push_back({our_line, Line::MERGED}); + } + // Case 5: All different - conflict + else { + Conflict conflict; + conflict.start_line = result.merged_lines.size(); + conflict.base_lines.push_back({base_line, Line::BASE}); + conflict.our_lines.push_back({our_line, Line::OURS}); + conflict.their_lines.push_back({their_line, Line::THEIRS}); + conflict.end_line = result.merged_lines.size(); + + // Perform context analysis using ours version as context + // (could also use base or theirs, but ours is typically most relevant) + conflict.context = analysis::analyze_context(ours, i, i); + + // Perform risk analysis for different resolution strategies + std::vector base_vec = {base_line}; + std::vector ours_vec = {our_line}; + std::vector theirs_vec = {their_line}; + + conflict.risk_ours = + analysis::analyze_risk_ours(base_vec, ours_vec, theirs_vec); + conflict.risk_theirs = + analysis::analyze_risk_theirs(base_vec, ours_vec, theirs_vec); + conflict.risk_both = + analysis::analyze_risk_both(base_vec, ours_vec, theirs_vec); + + result.conflicts.push_back(conflict); + + // Add conflict markers + result.merged_lines.push_back({"<<<<<<< OURS", Line::MERGED}); + result.merged_lines.push_back({our_line, Line::OURS}); + result.merged_lines.push_back({"=======", Line::MERGED}); + result.merged_lines.push_back({their_line, Line::THEIRS}); + result.merged_lines.push_back({">>>>>>> THEIRS", Line::MERGED}); + } + } + + return result; } -} // namespace merge -} // namespace wizardmerge +MergeResult auto_resolve(const MergeResult &result) { + MergeResult resolved = result; + + // Auto-resolve whitespace-only differences + std::vector remaining_conflicts; + + for (const auto &conflict : result.conflicts) { + bool can_resolve = false; + + // Check if differences are whitespace-only + if (conflict.our_lines.size() == conflict.their_lines.size()) { + can_resolve = true; + for (size_t i = 0; i < conflict.our_lines.size(); ++i) { + if (!lines_equal_ignore_whitespace(conflict.our_lines[i].content, + conflict.their_lines[i].content)) { + can_resolve = false; + break; + } + } + } + + if (!can_resolve) { + remaining_conflicts.push_back(conflict); + } + } + + resolved.conflicts = remaining_conflicts; + return resolved; +} + +} // namespace merge +} // namespace wizardmerge diff --git a/backend/tests/test_context_analyzer.cpp b/backend/tests/test_context_analyzer.cpp index c3e66d7..7bfc3ec 100644 --- a/backend/tests/test_context_analyzer.cpp +++ b/backend/tests/test_context_analyzer.cpp @@ -12,209 +12,167 @@ using namespace wizardmerge::analysis; * Test basic context analysis */ TEST(ContextAnalyzerTest, BasicContextAnalysis) { - std::vector lines = { - "#include ", - "", - "class MyClass {", - "public:", - " void myMethod() {", - " int x = 42;", - " int y = 100;", - " return;", - " }", - "};" - }; - - auto context = analyze_context(lines, 5, 7); - - EXPECT_EQ(context.start_line, 5); - EXPECT_EQ(context.end_line, 7); - EXPECT_FALSE(context.surrounding_lines.empty()); + std::vector lines = {"#include ", + "", + "class MyClass {", + "public:", + " void myMethod() {", + " int x = 42;", + " int y = 100;", + " return;", + " }", + "};"}; + + auto context = analyze_context(lines, 5, 7); + + EXPECT_EQ(context.start_line, 5); + EXPECT_EQ(context.end_line, 7); + EXPECT_FALSE(context.surrounding_lines.empty()); } /** * Test function name extraction */ TEST(ContextAnalyzerTest, ExtractFunctionName) { - std::vector lines = { - "void testFunction() {", - " int x = 10;", - " return;", - "}" - }; - - std::string func_name = extract_function_name(lines, 1); - EXPECT_EQ(func_name, "testFunction"); + std::vector lines = {"void testFunction() {", " int x = 10;", + " return;", "}"}; + + std::string func_name = extract_function_name(lines, 1); + EXPECT_EQ(func_name, "testFunction"); } /** * Test Python function name extraction */ TEST(ContextAnalyzerTest, ExtractPythonFunctionName) { - std::vector lines = { - "def my_python_function():", - " x = 10", - " return x" - }; - - std::string func_name = extract_function_name(lines, 1); - EXPECT_EQ(func_name, "my_python_function"); + std::vector lines = {"def my_python_function():", " x = 10", + " return x"}; + + std::string func_name = extract_function_name(lines, 1); + EXPECT_EQ(func_name, "my_python_function"); } /** * Test class name extraction */ TEST(ContextAnalyzerTest, ExtractClassName) { - std::vector lines = { - "class TestClass {", - " int member;", - "};" - }; - - std::string class_name = extract_class_name(lines, 1); - EXPECT_EQ(class_name, "TestClass"); + std::vector lines = {"class TestClass {", " int member;", + "};"}; + + std::string class_name = extract_class_name(lines, 1); + EXPECT_EQ(class_name, "TestClass"); } /** * Test import extraction */ TEST(ContextAnalyzerTest, ExtractImports) { - std::vector lines = { - "#include ", - "#include ", - "", - "int main() {", - " return 0;", - "}" - }; - - auto imports = extract_imports(lines); - EXPECT_EQ(imports.size(), 2); - EXPECT_EQ(imports[0], "#include "); - EXPECT_EQ(imports[1], "#include "); + std::vector lines = { + "#include ", "#include ", "", + "int main() {", " return 0;", "}"}; + + auto imports = extract_imports(lines); + EXPECT_EQ(imports.size(), 2); + EXPECT_EQ(imports[0], "#include "); + EXPECT_EQ(imports[1], "#include "); } /** * Test context with no function */ TEST(ContextAnalyzerTest, NoFunctionContext) { - std::vector lines = { - "int x = 10;", - "int y = 20;" - }; - - std::string func_name = extract_function_name(lines, 0); - EXPECT_EQ(func_name, ""); + std::vector lines = {"int x = 10;", "int y = 20;"}; + + std::string func_name = extract_function_name(lines, 0); + EXPECT_EQ(func_name, ""); } /** * Test context window boundaries */ TEST(ContextAnalyzerTest, ContextWindowBoundaries) { - std::vector lines = { - "line1", - "line2", - "line3", - "line4", - "line5" - }; - - // Test with small context window at beginning of file - auto context = analyze_context(lines, 0, 0, 2); - EXPECT_GE(context.surrounding_lines.size(), 1); - - // Test with context window at end of file - context = analyze_context(lines, 4, 4, 2); - EXPECT_GE(context.surrounding_lines.size(), 1); + std::vector lines = {"line1", "line2", "line3", "line4", + "line5"}; + + // Test with small context window at beginning of file + auto context = analyze_context(lines, 0, 0, 2); + EXPECT_GE(context.surrounding_lines.size(), 1); + + // Test with context window at end of file + context = analyze_context(lines, 4, 4, 2); + EXPECT_GE(context.surrounding_lines.size(), 1); } /** * Test TypeScript function detection */ TEST(ContextAnalyzerTest, TypeScriptFunctionDetection) { - std::vector lines = { - "export async function fetchData() {", - " const data = await api.get();", - " return data;", - "}" - }; - - std::string func_name = extract_function_name(lines, 1); - EXPECT_EQ(func_name, "fetchData"); + std::vector lines = {"export async function fetchData() {", + " const data = await api.get();", + " return data;", "}"}; + + std::string func_name = extract_function_name(lines, 1); + EXPECT_EQ(func_name, "fetchData"); } /** * Test TypeScript arrow function detection */ TEST(ContextAnalyzerTest, TypeScriptArrowFunctionDetection) { - std::vector lines = { - "const handleClick = (event: MouseEvent) => {", - " console.log(event);", - "};" - }; - - std::string func_name = extract_function_name(lines, 0); - EXPECT_EQ(func_name, "handleClick"); + std::vector lines = { + "const handleClick = (event: MouseEvent) => {", " console.log(event);", + "};"}; + + std::string func_name = extract_function_name(lines, 0); + EXPECT_EQ(func_name, "handleClick"); } /** * Test TypeScript interface detection */ TEST(ContextAnalyzerTest, TypeScriptInterfaceDetection) { - std::vector lines = { - "export interface User {", - " id: number;", - " name: string;", - "}" - }; - - std::string class_name = extract_class_name(lines, 1); - EXPECT_EQ(class_name, "User"); + std::vector lines = { + "export interface User {", " id: number;", " name: string;", "}"}; + + std::string class_name = extract_class_name(lines, 1); + EXPECT_EQ(class_name, "User"); } /** * Test TypeScript type alias detection */ TEST(ContextAnalyzerTest, TypeScriptTypeAliasDetection) { - std::vector lines = { - "export type Status = 'pending' | 'approved' | 'rejected';", - "const status: Status = 'pending';" - }; - - std::string type_name = extract_class_name(lines, 0); - EXPECT_EQ(type_name, "Status"); + std::vector lines = { + "export type Status = 'pending' | 'approved' | 'rejected';", + "const status: Status = 'pending';"}; + + std::string type_name = extract_class_name(lines, 0); + EXPECT_EQ(type_name, "Status"); } /** * Test TypeScript enum detection */ TEST(ContextAnalyzerTest, TypeScriptEnumDetection) { - std::vector lines = { - "enum Color {", - " Red,", - " Green,", - " Blue", - "}" - }; - - std::string enum_name = extract_class_name(lines, 1); - EXPECT_EQ(enum_name, "Color"); + std::vector lines = {"enum Color {", " Red,", " Green,", + " Blue", "}"}; + + std::string enum_name = extract_class_name(lines, 1); + EXPECT_EQ(enum_name, "Color"); } /** * Test TypeScript import extraction */ TEST(ContextAnalyzerTest, TypeScriptImportExtraction) { - std::vector lines = { - "import { Component } from 'react';", - "import type { User } from './types';", - "import * as utils from './utils';", - "", - "function MyComponent() {", - " return null;", - "}" - }; - - auto imports = extract_imports(lines); - EXPECT_GE(imports.size(), 3); + std::vector lines = {"import { Component } from 'react';", + "import type { User } from './types';", + "import * as utils from './utils';", + "", + "function MyComponent() {", + " return null;", + "}"}; + + auto imports = extract_imports(lines); + EXPECT_GE(imports.size(), 3); } diff --git a/backend/tests/test_git_cli.cpp b/backend/tests/test_git_cli.cpp index bed9758..49c0f8a 100644 --- a/backend/tests/test_git_cli.cpp +++ b/backend/tests/test_git_cli.cpp @@ -4,203 +4,220 @@ */ #include "wizardmerge/git/git_cli.h" -#include #include #include +#include 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 using std::filesystem - std::filesystem::path temp_base = std::filesystem::temp_directory_path(); - test_dir = (temp_base / ("wizardmerge_git_test_" + std::to_string(time(nullptr)))).string(); - 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(); + std::string test_dir; + + void SetUp() override { + // Create temporary test directory using std::filesystem + std::filesystem::path temp_base = std::filesystem::temp_directory_path(); + test_dir = + (temp_base / ("wizardmerge_git_test_" + std::to_string(time(nullptr)))) + .string(); + 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()); + // 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")); + 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"); + 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")); + 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; + 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; + 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()); + 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"); + 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); + 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); } diff --git a/backend/tests/test_git_platform_client.cpp b/backend/tests/test_git_platform_client.cpp index d9094c7..535beac 100644 --- a/backend/tests/test_git_platform_client.cpp +++ b/backend/tests/test_git_platform_client.cpp @@ -12,105 +12,121 @@ using namespace wizardmerge::git; * Test PR URL parsing with various GitHub formats */ TEST(GitPlatformClientTest, ParseGitHubPRUrl_ValidUrls) { - GitPlatform platform; - std::string owner, repo; - int pr_number; - - // Test full HTTPS URL - ASSERT_TRUE(parse_pr_url("https://github.com/owner/repo/pull/123", platform, owner, repo, pr_number)); - EXPECT_EQ(platform, GitPlatform::GitHub); - 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", platform, owner, repo, pr_number)); - EXPECT_EQ(platform, GitPlatform::GitHub); - 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", platform, owner, repo, pr_number)); - EXPECT_EQ(platform, GitPlatform::GitHub); - EXPECT_EQ(owner, "testuser"); - EXPECT_EQ(repo, "testrepo"); - EXPECT_EQ(pr_number, 789); + GitPlatform platform; + std::string owner, repo; + int pr_number; + + // Test full HTTPS URL + ASSERT_TRUE(parse_pr_url("https://github.com/owner/repo/pull/123", platform, + owner, repo, pr_number)); + EXPECT_EQ(platform, GitPlatform::GitHub); + 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", platform, owner, + repo, pr_number)); + EXPECT_EQ(platform, GitPlatform::GitHub); + 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", + platform, owner, repo, pr_number)); + EXPECT_EQ(platform, GitPlatform::GitHub); + EXPECT_EQ(owner, "testuser"); + EXPECT_EQ(repo, "testrepo"); + EXPECT_EQ(pr_number, 789); } /** * Test GitLab MR URL parsing with various formats */ TEST(GitPlatformClientTest, ParseGitLabMRUrl_ValidUrls) { - GitPlatform platform; - std::string owner, repo; - int pr_number; - - // Test full HTTPS URL - ASSERT_TRUE(parse_pr_url("https://gitlab.com/owner/repo/-/merge_requests/123", platform, owner, repo, pr_number)); - EXPECT_EQ(platform, GitPlatform::GitLab); - EXPECT_EQ(owner, "owner"); - EXPECT_EQ(repo, "repo"); - EXPECT_EQ(pr_number, 123); - - // Test with group/subgroup/project - ASSERT_TRUE(parse_pr_url("https://gitlab.com/group/subgroup/project/-/merge_requests/456", platform, owner, repo, pr_number)); - EXPECT_EQ(platform, GitPlatform::GitLab); - EXPECT_EQ(owner, "group/subgroup"); - EXPECT_EQ(repo, "project"); - EXPECT_EQ(pr_number, 456); - - // Test without https:// - ASSERT_TRUE(parse_pr_url("gitlab.com/mygroup/myproject/-/merge_requests/789", platform, owner, repo, pr_number)); - EXPECT_EQ(platform, GitPlatform::GitLab); - EXPECT_EQ(owner, "mygroup"); - EXPECT_EQ(repo, "myproject"); - EXPECT_EQ(pr_number, 789); + GitPlatform platform; + std::string owner, repo; + int pr_number; + + // Test full HTTPS URL + ASSERT_TRUE(parse_pr_url("https://gitlab.com/owner/repo/-/merge_requests/123", + platform, owner, repo, pr_number)); + EXPECT_EQ(platform, GitPlatform::GitLab); + EXPECT_EQ(owner, "owner"); + EXPECT_EQ(repo, "repo"); + EXPECT_EQ(pr_number, 123); + + // Test with group/subgroup/project + ASSERT_TRUE(parse_pr_url( + "https://gitlab.com/group/subgroup/project/-/merge_requests/456", + platform, owner, repo, pr_number)); + EXPECT_EQ(platform, GitPlatform::GitLab); + EXPECT_EQ(owner, "group/subgroup"); + EXPECT_EQ(repo, "project"); + EXPECT_EQ(pr_number, 456); + + // Test without https:// + ASSERT_TRUE(parse_pr_url("gitlab.com/mygroup/myproject/-/merge_requests/789", + platform, owner, repo, pr_number)); + EXPECT_EQ(platform, GitPlatform::GitLab); + EXPECT_EQ(owner, "mygroup"); + EXPECT_EQ(repo, "myproject"); + EXPECT_EQ(pr_number, 789); } /** * Test PR/MR URL parsing with invalid formats */ TEST(GitPlatformClientTest, ParsePRUrl_InvalidUrls) { - GitPlatform platform; - std::string owner, repo; - int pr_number; - - // Missing PR number - EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo/pull/", platform, owner, repo, pr_number)); - - // Invalid format - EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo", platform, owner, repo, pr_number)); - - // Not a GitHub or GitLab URL - EXPECT_FALSE(parse_pr_url("https://bitbucket.org/owner/repo/pull-requests/123", platform, owner, repo, pr_number)); - - // Empty string - EXPECT_FALSE(parse_pr_url("", platform, owner, repo, pr_number)); - - // Wrong path for GitLab - EXPECT_FALSE(parse_pr_url("https://gitlab.com/owner/repo/pull/123", platform, owner, repo, pr_number)); + GitPlatform platform; + std::string owner, repo; + int pr_number; + + // Missing PR number + EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo/pull/", platform, + owner, repo, pr_number)); + + // Invalid format + EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo", platform, owner, + repo, pr_number)); + + // Not a GitHub or GitLab URL + EXPECT_FALSE( + parse_pr_url("https://bitbucket.org/owner/repo/pull-requests/123", + platform, owner, repo, pr_number)); + + // Empty string + EXPECT_FALSE(parse_pr_url("", platform, owner, repo, pr_number)); + + // Wrong path for GitLab + EXPECT_FALSE(parse_pr_url("https://gitlab.com/owner/repo/pull/123", platform, + owner, repo, pr_number)); } /** * Test PR/MR URL with special characters in owner/repo names */ TEST(GitPlatformClientTest, ParsePRUrl_SpecialCharacters) { - GitPlatform platform; - std::string owner, repo; - int pr_number; - - // GitHub: Underscores and hyphens - ASSERT_TRUE(parse_pr_url("https://github.com/my-owner_123/my-repo_456/pull/999", platform, owner, repo, pr_number)); - EXPECT_EQ(platform, GitPlatform::GitHub); - EXPECT_EQ(owner, "my-owner_123"); - EXPECT_EQ(repo, "my-repo_456"); - EXPECT_EQ(pr_number, 999); - - // GitLab: Complex group paths - ASSERT_TRUE(parse_pr_url("https://gitlab.com/org-name/team-1/my_project/-/merge_requests/100", platform, owner, repo, pr_number)); - EXPECT_EQ(platform, GitPlatform::GitLab); - EXPECT_EQ(owner, "org-name/team-1"); - EXPECT_EQ(repo, "my_project"); - EXPECT_EQ(pr_number, 100); + GitPlatform platform; + std::string owner, repo; + int pr_number; + + // GitHub: Underscores and hyphens + ASSERT_TRUE( + parse_pr_url("https://github.com/my-owner_123/my-repo_456/pull/999", + platform, owner, repo, pr_number)); + EXPECT_EQ(platform, GitPlatform::GitHub); + EXPECT_EQ(owner, "my-owner_123"); + EXPECT_EQ(repo, "my-repo_456"); + EXPECT_EQ(pr_number, 999); + + // GitLab: Complex group paths + ASSERT_TRUE(parse_pr_url( + "https://gitlab.com/org-name/team-1/my_project/-/merge_requests/100", + platform, owner, repo, pr_number)); + EXPECT_EQ(platform, GitPlatform::GitLab); + EXPECT_EQ(owner, "org-name/team-1"); + EXPECT_EQ(repo, "my_project"); + EXPECT_EQ(pr_number, 100); } diff --git a/backend/tests/test_risk_analyzer.cpp b/backend/tests/test_risk_analyzer.cpp index 28cfd8d..2efa2b2 100644 --- a/backend/tests/test_risk_analyzer.cpp +++ b/backend/tests/test_risk_analyzer.cpp @@ -12,257 +12,228 @@ using namespace wizardmerge::analysis; * Test risk level to string conversion */ TEST(RiskAnalyzerTest, RiskLevelToString) { - EXPECT_EQ(risk_level_to_string(RiskLevel::LOW), "low"); - EXPECT_EQ(risk_level_to_string(RiskLevel::MEDIUM), "medium"); - EXPECT_EQ(risk_level_to_string(RiskLevel::HIGH), "high"); - EXPECT_EQ(risk_level_to_string(RiskLevel::CRITICAL), "critical"); + EXPECT_EQ(risk_level_to_string(RiskLevel::LOW), "low"); + EXPECT_EQ(risk_level_to_string(RiskLevel::MEDIUM), "medium"); + EXPECT_EQ(risk_level_to_string(RiskLevel::HIGH), "high"); + EXPECT_EQ(risk_level_to_string(RiskLevel::CRITICAL), "critical"); } /** * Test basic risk analysis for "ours" */ TEST(RiskAnalyzerTest, BasicRiskAnalysisOurs) { - std::vector base = {"int x = 10;"}; - std::vector ours = {"int x = 20;"}; - std::vector theirs = {"int x = 30;"}; - - auto risk = analyze_risk_ours(base, ours, theirs); - - EXPECT_TRUE(risk.level == RiskLevel::LOW || risk.level == RiskLevel::MEDIUM); - EXPECT_GE(risk.confidence_score, 0.0); - EXPECT_LE(risk.confidence_score, 1.0); - EXPECT_FALSE(risk.recommendations.empty()); + std::vector base = {"int x = 10;"}; + std::vector ours = {"int x = 20;"}; + std::vector theirs = {"int x = 30;"}; + + auto risk = analyze_risk_ours(base, ours, theirs); + + EXPECT_TRUE(risk.level == RiskLevel::LOW || risk.level == RiskLevel::MEDIUM); + EXPECT_GE(risk.confidence_score, 0.0); + EXPECT_LE(risk.confidence_score, 1.0); + EXPECT_FALSE(risk.recommendations.empty()); } /** * Test basic risk analysis for "theirs" */ TEST(RiskAnalyzerTest, BasicRiskAnalysisTheirs) { - std::vector base = {"int x = 10;"}; - std::vector ours = {"int x = 20;"}; - std::vector theirs = {"int x = 30;"}; - - auto risk = analyze_risk_theirs(base, ours, theirs); - - EXPECT_TRUE(risk.level == RiskLevel::LOW || risk.level == RiskLevel::MEDIUM); - EXPECT_GE(risk.confidence_score, 0.0); - EXPECT_LE(risk.confidence_score, 1.0); - EXPECT_FALSE(risk.recommendations.empty()); + std::vector base = {"int x = 10;"}; + std::vector ours = {"int x = 20;"}; + std::vector theirs = {"int x = 30;"}; + + auto risk = analyze_risk_theirs(base, ours, theirs); + + EXPECT_TRUE(risk.level == RiskLevel::LOW || risk.level == RiskLevel::MEDIUM); + EXPECT_GE(risk.confidence_score, 0.0); + EXPECT_LE(risk.confidence_score, 1.0); + EXPECT_FALSE(risk.recommendations.empty()); } /** * Test risk analysis for "both" (concatenation) */ TEST(RiskAnalyzerTest, RiskAnalysisBoth) { - std::vector base = {"int x = 10;"}; - std::vector ours = {"int x = 20;"}; - std::vector theirs = {"int x = 30;"}; - - auto risk = analyze_risk_both(base, ours, theirs); - - // "Both" strategy should typically have medium or higher risk - EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM); - EXPECT_GE(risk.confidence_score, 0.0); - EXPECT_LE(risk.confidence_score, 1.0); - EXPECT_FALSE(risk.recommendations.empty()); + std::vector base = {"int x = 10;"}; + std::vector ours = {"int x = 20;"}; + std::vector theirs = {"int x = 30;"}; + + auto risk = analyze_risk_both(base, ours, theirs); + + // "Both" strategy should typically have medium or higher risk + EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM); + EXPECT_GE(risk.confidence_score, 0.0); + EXPECT_LE(risk.confidence_score, 1.0); + EXPECT_FALSE(risk.recommendations.empty()); } /** * Test critical pattern detection */ TEST(RiskAnalyzerTest, DetectCriticalPatterns) { - std::vector safe_code = {"int x = 10;", "return x;"}; - std::vector unsafe_code = {"delete ptr;", "system(\"rm -rf /\");"}; - - EXPECT_FALSE(contains_critical_patterns(safe_code)); - EXPECT_TRUE(contains_critical_patterns(unsafe_code)); + std::vector safe_code = {"int x = 10;", "return x;"}; + std::vector unsafe_code = {"delete ptr;", + "system(\"rm -rf /\");"}; + + EXPECT_FALSE(contains_critical_patterns(safe_code)); + EXPECT_TRUE(contains_critical_patterns(unsafe_code)); } /** * Test API signature change detection */ TEST(RiskAnalyzerTest, DetectAPISignatureChanges) { - std::vector base_sig = {"void myFunction(int x) {"}; - std::vector modified_sig = {"void myFunction(int x, int y) {"}; - std::vector same_sig = {"void myFunction(int x) {"}; - - EXPECT_TRUE(has_api_signature_changes(base_sig, modified_sig)); - EXPECT_FALSE(has_api_signature_changes(base_sig, same_sig)); + std::vector base_sig = {"void myFunction(int x) {"}; + std::vector modified_sig = {"void myFunction(int x, int y) {"}; + std::vector same_sig = {"void myFunction(int x) {"}; + + EXPECT_TRUE(has_api_signature_changes(base_sig, modified_sig)); + EXPECT_FALSE(has_api_signature_changes(base_sig, same_sig)); } /** * Test high risk for large changes */ TEST(RiskAnalyzerTest, HighRiskForLargeChanges) { - std::vector base = {"line1"}; - std::vector ours; - std::vector theirs = {"line1"}; - - // Create large change in ours - for (int i = 0; i < 15; ++i) { - ours.push_back("changed_line_" + std::to_string(i)); - } - - auto risk = analyze_risk_ours(base, ours, theirs); - - // Should detect significant changes - EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM); - EXPECT_FALSE(risk.risk_factors.empty()); + std::vector base = {"line1"}; + std::vector ours; + std::vector theirs = {"line1"}; + + // Create large change in ours + for (int i = 0; i < 15; ++i) { + ours.push_back("changed_line_" + std::to_string(i)); + } + + auto risk = analyze_risk_ours(base, ours, theirs); + + // Should detect significant changes + EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM); + EXPECT_FALSE(risk.risk_factors.empty()); } /** * Test risk with critical patterns */ TEST(RiskAnalyzerTest, CriticalPatternsIncreaseRisk) { - std::vector base = {"int x = 10;"}; - std::vector ours = {"delete database;", "eval(user_input);"}; - std::vector theirs = {"int x = 10;"}; - - auto risk = analyze_risk_ours(base, ours, theirs); - - EXPECT_TRUE(risk.level >= RiskLevel::HIGH); - EXPECT_TRUE(risk.affects_critical_section); - EXPECT_FALSE(risk.risk_factors.empty()); + std::vector base = {"int x = 10;"}; + std::vector ours = {"delete database;", "eval(user_input);"}; + std::vector theirs = {"int x = 10;"}; + + auto risk = analyze_risk_ours(base, ours, theirs); + + EXPECT_TRUE(risk.level >= RiskLevel::HIGH); + EXPECT_TRUE(risk.affects_critical_section); + EXPECT_FALSE(risk.risk_factors.empty()); } /** * Test risk factors are populated */ TEST(RiskAnalyzerTest, RiskFactorsPopulated) { - std::vector base = {"line1", "line2", "line3"}; - std::vector ours = {"changed1", "changed2", "changed3"}; - std::vector theirs = {"line1", "line2", "line3"}; - - auto risk = analyze_risk_ours(base, ours, theirs); - - // Should have some analysis results - EXPECT_TRUE(!risk.recommendations.empty() || !risk.risk_factors.empty()); + std::vector base = {"line1", "line2", "line3"}; + std::vector ours = {"changed1", "changed2", "changed3"}; + std::vector theirs = {"line1", "line2", "line3"}; + + auto risk = analyze_risk_ours(base, ours, theirs); + + // Should have some analysis results + EXPECT_TRUE(!risk.recommendations.empty() || !risk.risk_factors.empty()); } /** * Test TypeScript interface change detection */ TEST(RiskAnalyzerTest, TypeScriptInterfaceChangesDetected) { - std::vector base = { - "interface User {", - " name: string;", - "}" - }; - std::vector modified = { - "interface User {", - " name: string;", - " age: number;", - "}" - }; - - EXPECT_TRUE(has_typescript_interface_changes(base, modified)); + std::vector base = {"interface User {", " name: string;", + "}"}; + std::vector modified = {"interface User {", " name: string;", + " age: number;", "}"}; + + EXPECT_TRUE(has_typescript_interface_changes(base, modified)); } /** * Test TypeScript type alias change detection */ TEST(RiskAnalyzerTest, TypeScriptTypeChangesDetected) { - std::vector base = { - "type Status = 'pending' | 'approved';" - }; - std::vector modified = { - "type Status = 'pending' | 'approved' | 'rejected';" - }; - - EXPECT_TRUE(has_typescript_interface_changes(base, modified)); + std::vector base = {"type Status = 'pending' | 'approved';"}; + std::vector modified = { + "type Status = 'pending' | 'approved' | 'rejected';"}; + + EXPECT_TRUE(has_typescript_interface_changes(base, modified)); } /** * Test TypeScript enum change detection */ TEST(RiskAnalyzerTest, TypeScriptEnumChangesDetected) { - std::vector base = { - "enum Color {", - " Red,", - " Green", - "}" - }; - std::vector modified = { - "enum Color {", - " Red,", - " Green,", - " Blue", - "}" - }; - - EXPECT_TRUE(has_typescript_interface_changes(base, modified)); + std::vector base = {"enum Color {", " Red,", " Green", + "}"}; + std::vector modified = {"enum Color {", " Red,", " Green,", + " Blue", "}"}; + + EXPECT_TRUE(has_typescript_interface_changes(base, modified)); } /** * Test package-lock.json file detection */ TEST(RiskAnalyzerTest, PackageLockFileDetection) { - EXPECT_TRUE(is_package_lock_file("package-lock.json")); - EXPECT_TRUE(is_package_lock_file("path/to/package-lock.json")); - EXPECT_TRUE(is_package_lock_file("yarn.lock")); - EXPECT_TRUE(is_package_lock_file("pnpm-lock.yaml")); - EXPECT_TRUE(is_package_lock_file("bun.lockb")); - EXPECT_FALSE(is_package_lock_file("package.json")); - EXPECT_FALSE(is_package_lock_file("src/index.ts")); + EXPECT_TRUE(is_package_lock_file("package-lock.json")); + EXPECT_TRUE(is_package_lock_file("path/to/package-lock.json")); + EXPECT_TRUE(is_package_lock_file("yarn.lock")); + EXPECT_TRUE(is_package_lock_file("pnpm-lock.yaml")); + EXPECT_TRUE(is_package_lock_file("bun.lockb")); + EXPECT_FALSE(is_package_lock_file("package.json")); + EXPECT_FALSE(is_package_lock_file("src/index.ts")); } /** * Test TypeScript critical patterns detection */ TEST(RiskAnalyzerTest, TypeScriptCriticalPatternsDetected) { - std::vector code_with_ts_issues = { - "const user = data as any;", - "// @ts-ignore", - "element.innerHTML = userInput;", - "localStorage.setItem('password', pwd);" - }; - - EXPECT_TRUE(contains_critical_patterns(code_with_ts_issues)); + std::vector code_with_ts_issues = { + "const user = data as any;", "// @ts-ignore", + "element.innerHTML = userInput;", + "localStorage.setItem('password', pwd);"}; + + EXPECT_TRUE(contains_critical_patterns(code_with_ts_issues)); } /** * Test TypeScript safe code doesn't trigger false positives */ TEST(RiskAnalyzerTest, TypeScriptSafeCodeNoFalsePositives) { - std::vector safe_code = { - "const user: User = { name: 'John', age: 30 };", - "function greet(name: string): string {", - " return `Hello, ${name}`;", - "}" - }; - - EXPECT_FALSE(contains_critical_patterns(safe_code)); + std::vector safe_code = { + "const user: User = { name: 'John', age: 30 };", + "function greet(name: string): string {", " return `Hello, ${name}`;", + "}"}; + + EXPECT_FALSE(contains_critical_patterns(safe_code)); } /** * Test risk analysis includes TypeScript interface changes */ TEST(RiskAnalyzerTest, RiskAnalysisIncludesTypeScriptChanges) { - std::vector base = { - "interface User {", - " name: string;", - "}" - }; - std::vector ours = { - "interface User {", - " name: string;", - " email: string;", - "}" - }; - std::vector theirs = base; - - auto risk = analyze_risk_ours(base, ours, theirs); - - EXPECT_TRUE(risk.has_api_changes); - EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM); - - // Check if TypeScript-related risk factor is mentioned - bool has_ts_risk = false; - for (const auto& factor : risk.risk_factors) { - if (factor.find("TypeScript") != std::string::npos) { - has_ts_risk = true; - break; - } + std::vector base = {"interface User {", " name: string;", + "}"}; + std::vector ours = {"interface User {", " name: string;", + " email: string;", "}"}; + std::vector theirs = base; + + auto risk = analyze_risk_ours(base, ours, theirs); + + EXPECT_TRUE(risk.has_api_changes); + EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM); + + // Check if TypeScript-related risk factor is mentioned + bool has_ts_risk = false; + for (const auto &factor : risk.risk_factors) { + if (factor.find("TypeScript") != std::string::npos) { + has_ts_risk = true; + break; } - EXPECT_TRUE(has_ts_risk); + } + EXPECT_TRUE(has_ts_risk); } diff --git a/backend/tests/test_three_way_merge.cpp b/backend/tests/test_three_way_merge.cpp index 35bc27e..6a36237 100644 --- a/backend/tests/test_three_way_merge.cpp +++ b/backend/tests/test_three_way_merge.cpp @@ -12,114 +12,114 @@ using namespace wizardmerge::merge; * Test basic three-way merge with no conflicts */ TEST(ThreeWayMergeTest, NoConflicts) { - std::vector base = {"line1", "line2", "line3"}; - std::vector ours = {"line1", "line2_modified", "line3"}; - std::vector theirs = {"line1", "line2", "line3_modified"}; - - auto result = three_way_merge(base, ours, theirs); - - EXPECT_FALSE(result.has_conflicts()); - ASSERT_EQ(result.merged_lines.size(), 3); - EXPECT_EQ(result.merged_lines[0].content, "line1"); - EXPECT_EQ(result.merged_lines[1].content, "line2_modified"); - EXPECT_EQ(result.merged_lines[2].content, "line3_modified"); + std::vector base = {"line1", "line2", "line3"}; + std::vector ours = {"line1", "line2_modified", "line3"}; + std::vector theirs = {"line1", "line2", "line3_modified"}; + + auto result = three_way_merge(base, ours, theirs); + + EXPECT_FALSE(result.has_conflicts()); + ASSERT_EQ(result.merged_lines.size(), 3); + EXPECT_EQ(result.merged_lines[0].content, "line1"); + EXPECT_EQ(result.merged_lines[1].content, "line2_modified"); + EXPECT_EQ(result.merged_lines[2].content, "line3_modified"); } /** * Test three-way merge with conflicts */ TEST(ThreeWayMergeTest, WithConflicts) { - std::vector base = {"line1", "line2", "line3"}; - std::vector ours = {"line1", "line2_ours", "line3"}; - std::vector theirs = {"line1", "line2_theirs", "line3"}; - - auto result = three_way_merge(base, ours, theirs); - - EXPECT_TRUE(result.has_conflicts()); - EXPECT_EQ(result.conflicts.size(), 1); + std::vector base = {"line1", "line2", "line3"}; + std::vector ours = {"line1", "line2_ours", "line3"}; + std::vector theirs = {"line1", "line2_theirs", "line3"}; + + auto result = three_way_merge(base, ours, theirs); + + EXPECT_TRUE(result.has_conflicts()); + EXPECT_EQ(result.conflicts.size(), 1); } /** * Test identical changes from both sides */ TEST(ThreeWayMergeTest, IdenticalChanges) { - std::vector base = {"line1", "line2", "line3"}; - std::vector ours = {"line1", "line2_same", "line3"}; - std::vector theirs = {"line1", "line2_same", "line3"}; - - auto result = three_way_merge(base, ours, theirs); - - EXPECT_FALSE(result.has_conflicts()); - EXPECT_EQ(result.merged_lines[1].content, "line2_same"); + std::vector base = {"line1", "line2", "line3"}; + std::vector ours = {"line1", "line2_same", "line3"}; + std::vector theirs = {"line1", "line2_same", "line3"}; + + auto result = three_way_merge(base, ours, theirs); + + EXPECT_FALSE(result.has_conflicts()); + EXPECT_EQ(result.merged_lines[1].content, "line2_same"); } /** * Test base equals ours, theirs changed */ TEST(ThreeWayMergeTest, BaseEqualsOurs) { - std::vector base = {"line1", "line2", "line3"}; - std::vector ours = {"line1", "line2", "line3"}; - std::vector theirs = {"line1", "line2_changed", "line3"}; - - auto result = three_way_merge(base, ours, theirs); - - EXPECT_FALSE(result.has_conflicts()); - EXPECT_EQ(result.merged_lines[1].content, "line2_changed"); + std::vector base = {"line1", "line2", "line3"}; + std::vector ours = {"line1", "line2", "line3"}; + std::vector theirs = {"line1", "line2_changed", "line3"}; + + auto result = three_way_merge(base, ours, theirs); + + EXPECT_FALSE(result.has_conflicts()); + EXPECT_EQ(result.merged_lines[1].content, "line2_changed"); } /** * Test base equals theirs, ours changed */ TEST(ThreeWayMergeTest, BaseEqualsTheirs) { - std::vector base = {"line1", "line2", "line3"}; - std::vector ours = {"line1", "line2_changed", "line3"}; - std::vector theirs = {"line1", "line2", "line3"}; - - auto result = three_way_merge(base, ours, theirs); - - EXPECT_FALSE(result.has_conflicts()); - EXPECT_EQ(result.merged_lines[1].content, "line2_changed"); + std::vector base = {"line1", "line2", "line3"}; + std::vector ours = {"line1", "line2_changed", "line3"}; + std::vector theirs = {"line1", "line2", "line3"}; + + auto result = three_way_merge(base, ours, theirs); + + EXPECT_FALSE(result.has_conflicts()); + EXPECT_EQ(result.merged_lines[1].content, "line2_changed"); } /** * Test auto-resolve whitespace differences */ TEST(AutoResolveTest, WhitespaceOnly) { - std::vector base = {"line1", "line2", "line3"}; - std::vector ours = {"line1", " line2_changed ", "line3"}; - std::vector theirs = {"line1", "line2_changed", "line3"}; - - auto result = three_way_merge(base, ours, theirs); - auto resolved = auto_resolve(result); - - // Whitespace-only differences should be auto-resolved - EXPECT_LT(resolved.conflicts.size(), result.conflicts.size()); + std::vector base = {"line1", "line2", "line3"}; + std::vector ours = {"line1", " line2_changed ", "line3"}; + std::vector theirs = {"line1", "line2_changed", "line3"}; + + auto result = three_way_merge(base, ours, theirs); + auto resolved = auto_resolve(result); + + // Whitespace-only differences should be auto-resolved + EXPECT_LT(resolved.conflicts.size(), result.conflicts.size()); } /** * Test empty files */ TEST(ThreeWayMergeTest, EmptyFiles) { - std::vector base = {}; - std::vector ours = {}; - std::vector theirs = {}; - - auto result = three_way_merge(base, ours, theirs); - - EXPECT_FALSE(result.has_conflicts()); - EXPECT_EQ(result.merged_lines.size(), 0); + std::vector base = {}; + std::vector ours = {}; + std::vector theirs = {}; + + auto result = three_way_merge(base, ours, theirs); + + EXPECT_FALSE(result.has_conflicts()); + EXPECT_EQ(result.merged_lines.size(), 0); } /** * Test one side adds lines */ TEST(ThreeWayMergeTest, OneSideAddsLines) { - std::vector base = {"line1"}; - std::vector ours = {"line1", "line2"}; - std::vector theirs = {"line1"}; - - auto result = three_way_merge(base, ours, theirs); - - EXPECT_FALSE(result.has_conflicts()); - ASSERT_EQ(result.merged_lines.size(), 2); + std::vector base = {"line1"}; + std::vector ours = {"line1", "line2"}; + std::vector theirs = {"line1"}; + + auto result = three_way_merge(base, ours, theirs); + + EXPECT_FALSE(result.has_conflicts()); + ASSERT_EQ(result.merged_lines.size(), 2); } diff --git a/frontends/cli/include/http_client.h b/frontends/cli/include/http_client.h index b5b8871..1c7c78c 100644 --- a/frontends/cli/include/http_client.h +++ b/frontends/cli/include/http_client.h @@ -1,62 +1,60 @@ #ifndef HTTP_CLIENT_H #define HTTP_CLIENT_H +#include #include #include -#include /** * @brief HTTP client for communicating with WizardMerge backend */ class HttpClient { public: - /** - * @brief Construct HTTP client with backend URL - * @param backendUrl URL of the backend server (e.g., "http://localhost:8080") - */ - explicit HttpClient(const std::string& backendUrl); + /** + * @brief Construct HTTP client with backend URL + * @param backendUrl URL of the backend server (e.g., "http://localhost:8080") + */ + explicit HttpClient(const std::string &backendUrl); - /** - * @brief Perform a three-way merge via backend API - * @param base Base version lines - * @param ours Our version lines - * @param theirs Their version lines - * @param merged Output merged lines - * @param hasConflicts Output whether conflicts were detected - * @return true if successful, false on error - */ - bool performMerge( - const std::vector& base, - const std::vector& ours, - const std::vector& theirs, - std::vector& merged, - bool& hasConflicts - ); + /** + * @brief Perform a three-way merge via backend API + * @param base Base version lines + * @param ours Our version lines + * @param theirs Their version lines + * @param merged Output merged lines + * @param hasConflicts Output whether conflicts were detected + * @return true if successful, false on error + */ + bool performMerge(const std::vector &base, + const std::vector &ours, + const std::vector &theirs, + std::vector &merged, bool &hasConflicts); - /** - * @brief Check if backend is reachable - * @return true if backend responds, false otherwise - */ - bool checkBackend(); + /** + * @brief Check if backend is reachable + * @return true if backend responds, false otherwise + */ + bool checkBackend(); - /** - * @brief Get last error message - * @return Error message string - */ - std::string getLastError() const { return lastError_; } + /** + * @brief Get last error message + * @return Error message string + */ + std::string getLastError() const { return lastError_; } private: - std::string backendUrl_; - std::string lastError_; + std::string backendUrl_; + std::string lastError_; - /** - * @brief Perform HTTP POST request - * @param endpoint API endpoint (e.g., "/api/merge") - * @param jsonBody JSON request body - * @param response Output response string - * @return true if successful, false on error - */ - bool post(const std::string& endpoint, const std::string& jsonBody, std::string& response); + /** + * @brief Perform HTTP POST request + * @param endpoint API endpoint (e.g., "/api/merge") + * @param jsonBody JSON request body + * @param response Output response string + * @return true if successful, false on error + */ + bool post(const std::string &endpoint, const std::string &jsonBody, + std::string &response); }; #endif // HTTP_CLIENT_H diff --git a/frontends/cli/src/http_client.cpp b/frontends/cli/src/http_client.cpp index cfedcfa..a203d52 100644 --- a/frontends/cli/src/http_client.cpp +++ b/frontends/cli/src/http_client.cpp @@ -1,142 +1,152 @@ #include "http_client.h" #include -#include #include +#include // Callback for libcurl to write response data -static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { - ((std::string*)userp)->append((char*)contents, size * nmemb); - return size * nmemb; +static size_t WriteCallback(void *contents, size_t size, size_t nmemb, + void *userp) { + ((std::string *)userp)->append((char *)contents, size * nmemb); + return size * nmemb; } -HttpClient::HttpClient(const std::string& backendUrl) - : backendUrl_(backendUrl), lastError_("") { +HttpClient::HttpClient(const std::string &backendUrl) + : backendUrl_(backendUrl), lastError_("") {} + +bool HttpClient::post(const std::string &endpoint, const std::string &jsonBody, + std::string &response) { + CURL *curl = curl_easy_init(); + if (!curl) { + lastError_ = "Failed to initialize CURL"; + return false; + } + + std::string url = backendUrl_ + endpoint; + response.clear(); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonBody.c_str()); + 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); + + bool success = (res == CURLE_OK); + if (!success) { + lastError_ = std::string("CURL error: ") + curl_easy_strerror(res); + } + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + return success; } -bool HttpClient::post(const std::string& endpoint, const std::string& jsonBody, std::string& response) { - CURL* curl = curl_easy_init(); - if (!curl) { - lastError_ = "Failed to initialize CURL"; - return false; +bool HttpClient::performMerge(const std::vector &base, + const std::vector &ours, + const std::vector &theirs, + std::vector &merged, + bool &hasConflicts) { + // Build JSON request + // NOTE: This is a simplified JSON builder for prototype purposes. + // LIMITATION: Does not escape special characters in strings (quotes, + // backslashes, etc.) + // TODO: For production, use a proper JSON library like nlohmann/json or + // rapidjson This implementation works for simple test cases but will fail + // with complex content. + std::ostringstream json; + json << "{"; + json << "\"base\":["; + for (size_t i = 0; i < base.size(); ++i) { + json << "\"" << base[i] << "\""; // WARNING: No escaping! + if (i < base.size() - 1) + json << ","; + } + json << "],"; + json << "\"ours\":["; + for (size_t i = 0; i < ours.size(); ++i) { + json << "\"" << ours[i] << "\""; // WARNING: No escaping! + if (i < ours.size() - 1) + json << ","; + } + json << "],"; + json << "\"theirs\":["; + for (size_t i = 0; i < theirs.size(); ++i) { + json << "\"" << theirs[i] << "\""; // WARNING: No escaping! + if (i < theirs.size() - 1) + json << ","; + } + json << "]"; + json << "}"; + + std::string response; + if (!post("/api/merge", json.str(), response)) { + return false; + } + + // Parse JSON response (simple parsing for now) + // NOTE: This is a fragile string-based parser for prototype purposes. + // LIMITATION: Will break on complex JSON or unexpected formatting. + // TODO: For production, use a proper JSON library like nlohmann/json or + // rapidjson + merged.clear(); + hasConflicts = (response.find("\"has_conflicts\":true") != std::string::npos); + + // Extract merged lines from response + // This is a simplified parser - production code MUST use a JSON library + size_t mergedPos = response.find("\"merged\":"); + if (mergedPos != std::string::npos) { + size_t startBracket = response.find("[", mergedPos); + size_t endBracket = response.find("]", startBracket); + if (startBracket != std::string::npos && endBracket != std::string::npos) { + std::string mergedArray = + response.substr(startBracket + 1, endBracket - startBracket - 1); + + // Parse lines (simplified) + size_t pos = 0; + while (pos < mergedArray.size()) { + size_t quoteStart = mergedArray.find("\"", pos); + if (quoteStart == std::string::npos) + break; + size_t quoteEnd = mergedArray.find("\"", quoteStart + 1); + if (quoteEnd == std::string::npos) + break; + + std::string line = + mergedArray.substr(quoteStart + 1, quoteEnd - quoteStart - 1); + merged.push_back(line); + pos = quoteEnd + 1; + } } + } - std::string url = backendUrl_ + endpoint; - response.clear(); - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonBody.c_str()); - 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); - - bool success = (res == CURLE_OK); - if (!success) { - lastError_ = std::string("CURL error: ") + curl_easy_strerror(res); - } - - curl_slist_free_all(headers); - curl_easy_cleanup(curl); - - return success; -} - -bool HttpClient::performMerge( - const std::vector& base, - const std::vector& ours, - const std::vector& theirs, - std::vector& merged, - bool& hasConflicts -) { - // Build JSON request - // NOTE: This is a simplified JSON builder for prototype purposes. - // LIMITATION: Does not escape special characters in strings (quotes, backslashes, etc.) - // TODO: For production, use a proper JSON library like nlohmann/json or rapidjson - // This implementation works for simple test cases but will fail with complex content. - std::ostringstream json; - json << "{"; - json << "\"base\":["; - for (size_t i = 0; i < base.size(); ++i) { - json << "\"" << base[i] << "\""; // WARNING: No escaping! - if (i < base.size() - 1) json << ","; - } - json << "],"; - json << "\"ours\":["; - for (size_t i = 0; i < ours.size(); ++i) { - json << "\"" << ours[i] << "\""; // WARNING: No escaping! - if (i < ours.size() - 1) json << ","; - } - json << "],"; - json << "\"theirs\":["; - for (size_t i = 0; i < theirs.size(); ++i) { - json << "\"" << theirs[i] << "\""; // WARNING: No escaping! - if (i < theirs.size() - 1) json << ","; - } - json << "]"; - json << "}"; - - std::string response; - if (!post("/api/merge", json.str(), response)) { - return false; - } - - // Parse JSON response (simple parsing for now) - // NOTE: This is a fragile string-based parser for prototype purposes. - // LIMITATION: Will break on complex JSON or unexpected formatting. - // TODO: For production, use a proper JSON library like nlohmann/json or rapidjson - merged.clear(); - hasConflicts = (response.find("\"has_conflicts\":true") != std::string::npos); - - // Extract merged lines from response - // This is a simplified parser - production code MUST use a JSON library - size_t mergedPos = response.find("\"merged\":"); - if (mergedPos != std::string::npos) { - size_t startBracket = response.find("[", mergedPos); - size_t endBracket = response.find("]", startBracket); - if (startBracket != std::string::npos && endBracket != std::string::npos) { - std::string mergedArray = response.substr(startBracket + 1, endBracket - startBracket - 1); - - // Parse lines (simplified) - size_t pos = 0; - while (pos < mergedArray.size()) { - size_t quoteStart = mergedArray.find("\"", pos); - if (quoteStart == std::string::npos) break; - size_t quoteEnd = mergedArray.find("\"", quoteStart + 1); - if (quoteEnd == std::string::npos) break; - - std::string line = mergedArray.substr(quoteStart + 1, quoteEnd - quoteStart - 1); - merged.push_back(line); - pos = quoteEnd + 1; - } - } - } - - return true; + return true; } bool HttpClient::checkBackend() { - CURL* curl = curl_easy_init(); - if (!curl) { - lastError_ = "Failed to initialize CURL"; - return false; - } + CURL *curl = curl_easy_init(); + if (!curl) { + lastError_ = "Failed to initialize CURL"; + return false; + } - std::string url = backendUrl_ + "/"; - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); + std::string url = backendUrl_ + "/"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); - CURLcode res = curl_easy_perform(curl); - bool success = (res == CURLE_OK); + CURLcode res = curl_easy_perform(curl); + bool success = (res == CURLE_OK); - if (!success) { - lastError_ = std::string("Cannot reach backend: ") + curl_easy_strerror(res); - } + if (!success) { + lastError_ = + std::string("Cannot reach backend: ") + curl_easy_strerror(res); + } - curl_easy_cleanup(curl); - return success; + curl_easy_cleanup(curl); + return success; } diff --git a/frontends/cli/src/main.cpp b/frontends/cli/src/main.cpp index 98f3bdc..df78a04 100644 --- a/frontends/cli/src/main.cpp +++ b/frontends/cli/src/main.cpp @@ -1,395 +1,423 @@ -#include "http_client.h" #include "file_utils.h" -#include +#include "http_client.h" +#include +#include +#include #include +#include #include #include -#include -#include -#include /** * @brief Print usage information */ -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 --ours --theirs \n"; - std::cout << " " << programName << " [OPTIONS] pr-resolve --url [--token ]\n"; - std::cout << " " << programName << " [OPTIONS] git-resolve [FILE]\n"; - std::cout << " " << programName << " --help\n"; - std::cout << " " << programName << " --version\n\n"; - std::cout << "Global Options:\n"; - std::cout << " --backend Backend server URL (default: http://localhost:8080)\n"; - std::cout << " -v, --verbose Enable verbose output\n"; - std::cout << " -q, --quiet Suppress non-error output\n"; - std::cout << " -h, --help Show this help message\n"; - std::cout << " --version Show version information\n\n"; - std::cout << "Commands:\n"; - std::cout << " merge Perform three-way merge\n"; - std::cout << " --base Base version file (required)\n"; - std::cout << " --ours Our version file (required)\n"; - std::cout << " --theirs Their version file (required)\n"; - std::cout << " -o, --output Output file (default: stdout)\n"; - std::cout << " --format Output format: text, json (default: text)\n\n"; - std::cout << " pr-resolve Resolve pull request conflicts\n"; - std::cout << " --url Pull request URL (required)\n"; - std::cout << " --token GitHub API token (optional, can use GITHUB_TOKEN env)\n"; - std::cout << " --branch Create branch with resolved conflicts (optional)\n"; - std::cout << " -o, --output 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"; +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 --ours --theirs \n"; + std::cout << " " << programName + << " [OPTIONS] pr-resolve --url [--token ]\n"; + std::cout << " " << programName << " [OPTIONS] git-resolve [FILE]\n"; + std::cout << " " << programName << " --help\n"; + std::cout << " " << programName << " --version\n\n"; + std::cout << "Global Options:\n"; + std::cout << " --backend Backend server URL (default: " + "http://localhost:8080)\n"; + std::cout << " -v, --verbose Enable verbose output\n"; + std::cout << " -q, --quiet Suppress non-error output\n"; + std::cout << " -h, --help Show this help message\n"; + std::cout << " --version Show version information\n\n"; + std::cout << "Commands:\n"; + std::cout << " merge Perform three-way merge\n"; + std::cout << " --base Base version file (required)\n"; + std::cout << " --ours Our version file (required)\n"; + std::cout << " --theirs Their version file (required)\n"; + std::cout << " -o, --output Output file (default: stdout)\n"; + std::cout << " --format Output format: text, json (default: " + "text)\n\n"; + std::cout << " pr-resolve Resolve pull request conflicts\n"; + std::cout << " --url Pull request URL (required)\n"; + std::cout << " --token GitHub API token (optional, can use " + "GITHUB_TOKEN env)\n"; + std::cout << " --branch Create branch with resolved conflicts " + "(optional)\n"; + std::cout << " -o, --output 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"; } /** * @brief Print version information */ void printVersion() { - std::cout << "WizardMerge CLI Frontend v1.0.0\n"; - std::cout << "Part of the WizardMerge Intelligent Merge Conflict Resolution system\n"; + std::cout << "WizardMerge CLI Frontend v1.0.0\n"; + std::cout << "Part of the WizardMerge Intelligent Merge Conflict Resolution " + "system\n"; } /** * @brief Parse command-line arguments and execute merge */ -int main(int argc, char* argv[]) { - // Default configuration - std::string backendUrl = "http://localhost:8080"; - bool verbose = false; - bool quiet = false; - std::string command; - std::string baseFile, oursFile, theirsFile, outputFile; - std::string format = "text"; - std::string prUrl, githubToken, branchName; +int main(int argc, char *argv[]) { + // Default configuration + std::string backendUrl = "http://localhost:8080"; + bool verbose = false; + bool quiet = false; + 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; - } + // Check environment variable + const char *envBackend = std::getenv("WIZARDMERGE_BACKEND"); + if (envBackend) { + backendUrl = envBackend; + } - // Parse arguments - for (int i = 1; i < argc; ++i) { - std::string arg = argv[i]; + // Check for GitHub token in environment + const char *envToken = std::getenv("GITHUB_TOKEN"); + if (envToken) { + githubToken = envToken; + } - if (arg == "--help" || arg == "-h") { - printUsage(argv[0]); - return 0; - } else if (arg == "--version") { - printVersion(); - return 0; - } else if (arg == "--backend") { - if (i + 1 < argc) { - backendUrl = argv[++i]; - } else { - std::cerr << "Error: --backend requires an argument\n"; - return 2; - } - } else if (arg == "--verbose" || arg == "-v") { - verbose = true; - } else if (arg == "--quiet" || arg == "-q") { - 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]; - } else { - std::cerr << "Error: --base requires an argument\n"; - return 2; - } - } else if (arg == "--ours") { - if (i + 1 < argc) { - oursFile = argv[++i]; - } else { - std::cerr << "Error: --ours requires an argument\n"; - return 2; - } - } else if (arg == "--theirs") { - if (i + 1 < argc) { - theirsFile = argv[++i]; - } else { - std::cerr << "Error: --theirs requires an argument\n"; - return 2; - } - } else if (arg == "--output" || arg == "-o") { - if (i + 1 < argc) { - outputFile = argv[++i]; - } else { - std::cerr << "Error: --output requires an argument\n"; - return 2; - } - } else if (arg == "--format") { - if (i + 1 < argc) { - format = argv[++i]; - } else { - std::cerr << "Error: --format requires an argument\n"; - return 2; - } - } - } + // Parse arguments + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; - // Check if command was provided - if (command.empty()) { - std::cerr << "Error: No command specified\n\n"; - printUsage(argv[0]); + if (arg == "--help" || arg == "-h") { + printUsage(argv[0]); + return 0; + } else if (arg == "--version") { + printVersion(); + return 0; + } else if (arg == "--backend") { + if (i + 1 < argc) { + backendUrl = argv[++i]; + } else { + std::cerr << "Error: --backend requires an argument\n"; return 2; + } + } else if (arg == "--verbose" || arg == "-v") { + verbose = true; + } else if (arg == "--quiet" || arg == "-q") { + 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]; + } else { + std::cerr << "Error: --base requires an argument\n"; + return 2; + } + } else if (arg == "--ours") { + if (i + 1 < argc) { + oursFile = argv[++i]; + } else { + std::cerr << "Error: --ours requires an argument\n"; + return 2; + } + } else if (arg == "--theirs") { + if (i + 1 < argc) { + theirsFile = argv[++i]; + } else { + std::cerr << "Error: --theirs requires an argument\n"; + return 2; + } + } else if (arg == "--output" || arg == "-o") { + if (i + 1 < argc) { + outputFile = argv[++i]; + } else { + std::cerr << "Error: --output requires an argument\n"; + return 2; + } + } else if (arg == "--format") { + if (i + 1 < argc) { + format = argv[++i]; + } else { + std::cerr << "Error: --format requires an argument\n"; + return 2; + } + } + } + + // Check if command was provided + if (command.empty()) { + std::cerr << "Error: No command specified\n\n"; + printUsage(argv[0]); + return 2; + } + + // Execute command + if (command == "merge") { + // Validate required arguments + if (baseFile.empty() || oursFile.empty() || theirsFile.empty()) { + std::cerr << "Error: merge command requires --base, --ours, and --theirs " + "arguments\n"; + return 2; } - // Execute command - if (command == "merge") { - // Validate required arguments - if (baseFile.empty() || oursFile.empty() || theirsFile.empty()) { - std::cerr << "Error: merge command requires --base, --ours, and --theirs arguments\n"; - return 2; - } + // Check files exist + if (!FileUtils::fileExists(baseFile)) { + std::cerr << "Error: Base file not found: " << baseFile << "\n"; + return 4; + } + if (!FileUtils::fileExists(oursFile)) { + std::cerr << "Error: Ours file not found: " << oursFile << "\n"; + return 4; + } + if (!FileUtils::fileExists(theirsFile)) { + std::cerr << "Error: Theirs file not found: " << theirsFile << "\n"; + return 4; + } - // Check files exist - if (!FileUtils::fileExists(baseFile)) { - std::cerr << "Error: Base file not found: " << baseFile << "\n"; - return 4; - } - if (!FileUtils::fileExists(oursFile)) { - std::cerr << "Error: Ours file not found: " << oursFile << "\n"; - return 4; - } - if (!FileUtils::fileExists(theirsFile)) { - std::cerr << "Error: Theirs file not found: " << theirsFile << "\n"; - return 4; - } + if (verbose) { + std::cout << "Backend URL: " << backendUrl << "\n"; + std::cout << "Base file: " << baseFile << "\n"; + std::cout << "Ours file: " << oursFile << "\n"; + std::cout << "Theirs file: " << theirsFile << "\n"; + } - if (verbose) { - std::cout << "Backend URL: " << backendUrl << "\n"; - std::cout << "Base file: " << baseFile << "\n"; - std::cout << "Ours file: " << oursFile << "\n"; - std::cout << "Theirs file: " << theirsFile << "\n"; - } + // Read input files + std::vector baseLines, oursLines, theirsLines; + if (!FileUtils::readLines(baseFile, baseLines)) { + std::cerr << "Error: Failed to read base file\n"; + return 4; + } + if (!FileUtils::readLines(oursFile, oursLines)) { + std::cerr << "Error: Failed to read ours file\n"; + return 4; + } + if (!FileUtils::readLines(theirsFile, theirsLines)) { + std::cerr << "Error: Failed to read theirs file\n"; + return 4; + } - // Read input files - std::vector baseLines, oursLines, theirsLines; - if (!FileUtils::readLines(baseFile, baseLines)) { - std::cerr << "Error: Failed to read base file\n"; - return 4; - } - if (!FileUtils::readLines(oursFile, oursLines)) { - std::cerr << "Error: Failed to read ours file\n"; - return 4; - } - if (!FileUtils::readLines(theirsFile, theirsLines)) { - std::cerr << "Error: Failed to read theirs file\n"; - return 4; - } + if (verbose) { + std::cout << "Read " << baseLines.size() << " lines from base\n"; + std::cout << "Read " << oursLines.size() << " lines from ours\n"; + std::cout << "Read " << theirsLines.size() << " lines from theirs\n"; + } - if (verbose) { - std::cout << "Read " << baseLines.size() << " lines from base\n"; - std::cout << "Read " << oursLines.size() << " lines from ours\n"; - std::cout << "Read " << theirsLines.size() << " lines from theirs\n"; - } + // Connect to backend and perform merge + HttpClient client(backendUrl); - // Connect to backend and perform merge - HttpClient client(backendUrl); - - if (!quiet) { - std::cout << "Connecting to backend: " << backendUrl << "\n"; - } + 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 (!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 << "Performing three-way merge...\n"; - } + if (!quiet) { + std::cout << "Performing three-way merge...\n"; + } - std::vector mergedLines; - bool hasConflicts = false; + std::vector mergedLines; + bool hasConflicts = false; - if (!client.performMerge(baseLines, oursLines, theirsLines, mergedLines, hasConflicts)) { - std::cerr << "Error: Merge failed: " << client.getLastError() << "\n"; - return 1; - } + if (!client.performMerge(baseLines, oursLines, theirsLines, mergedLines, + hasConflicts)) { + std::cerr << "Error: Merge failed: " << client.getLastError() << "\n"; + return 1; + } - // Output results - if (!quiet) { - std::cout << "Merge completed. Has conflicts: " << (hasConflicts ? "Yes" : "No") << "\n"; - std::cout << "Result has " << mergedLines.size() << " lines\n"; - } + // Output results + if (!quiet) { + std::cout << "Merge completed. Has conflicts: " + << (hasConflicts ? "Yes" : "No") << "\n"; + std::cout << "Result has " << mergedLines.size() << " lines\n"; + } - // Write output - if (outputFile.empty()) { - // Write to stdout - for (const auto& line : mergedLines) { - std::cout << line << "\n"; - } - } else { - if (!FileUtils::writeLines(outputFile, mergedLines)) { - std::cerr << "Error: Failed to write output file\n"; - return 4; - } - if (!quiet) { - std::cout << "Output written to: " << outputFile << "\n"; - } - } - - 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; + // Write output + if (outputFile.empty()) { + // Write to stdout + for (const auto &line : mergedLines) { + std::cout << line << "\n"; + } } else { - std::cerr << "Error: Unknown command: " << command << "\n"; - return 2; + if (!FileUtils::writeLines(outputFile, mergedLines)) { + std::cerr << "Error: Failed to write output file\n"; + return 4; + } + if (!quiet) { + std::cout << "Output written to: " << outputFile << "\n"; + } } - return 0; + 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; + } else { + std::cerr << "Error: Unknown command: " << command << "\n"; + return 2; + } + + return 0; } diff --git a/frontends/qt6/src/main.cpp b/frontends/qt6/src/main.cpp index 56d1f58..fe8a26e 100644 --- a/frontends/qt6/src/main.cpp +++ b/frontends/qt6/src/main.cpp @@ -1,88 +1,87 @@ +#include #include +#include #include #include -#include -#include #include #include /** * @brief Main entry point for WizardMerge Qt6 frontend - * + * * This application provides a native desktop interface for WizardMerge, * supporting both standalone mode (with embedded backend) and client mode * (connecting to a remote backend server). */ -int main(int argc, char *argv[]) -{ - QGuiApplication app(argc, argv); - app.setApplicationName("WizardMerge"); - app.setApplicationVersion("1.0.0"); - app.setOrganizationName("WizardMerge"); - app.setOrganizationDomain("wizardmerge.dev"); +int main(int argc, char *argv[]) { + QGuiApplication app(argc, argv); + app.setApplicationName("WizardMerge"); + app.setApplicationVersion("1.0.0"); + app.setOrganizationName("WizardMerge"); + app.setOrganizationDomain("wizardmerge.dev"); - // Command line parser - QCommandLineParser parser; - parser.setApplicationDescription("WizardMerge - Intelligent Merge Conflict Resolution"); - parser.addHelpOption(); - parser.addVersionOption(); + // Command line parser + QCommandLineParser parser; + parser.setApplicationDescription( + "WizardMerge - Intelligent Merge Conflict Resolution"); + parser.addHelpOption(); + parser.addVersionOption(); - QCommandLineOption backendUrlOption( - QStringList() << "b" << "backend-url", - "Backend server URL (default: http://localhost:8080)", - "url", - "http://localhost:8080" - ); - parser.addOption(backendUrlOption); + QCommandLineOption backendUrlOption( + QStringList() << "b" << "backend-url", + "Backend server URL (default: http://localhost:8080)", "url", + "http://localhost:8080"); + parser.addOption(backendUrlOption); - QCommandLineOption standaloneOption( - QStringList() << "s" << "standalone", - "Run in standalone mode with embedded backend" - ); - parser.addOption(standaloneOption); + QCommandLineOption standaloneOption( + QStringList() << "s" << "standalone", + "Run in standalone mode with embedded backend"); + parser.addOption(standaloneOption); - parser.addPositionalArgument("file", "File to open (optional)"); + parser.addPositionalArgument("file", "File to open (optional)"); - parser.process(app); + parser.process(app); - // Get command line arguments - QString backendUrl = parser.value(backendUrlOption); - bool standalone = parser.isSet(standaloneOption); - QStringList positionalArgs = parser.positionalArguments(); - QString filePath = positionalArgs.isEmpty() ? QString() : positionalArgs.first(); + // Get command line arguments + QString backendUrl = parser.value(backendUrlOption); + bool standalone = parser.isSet(standaloneOption); + QStringList positionalArgs = parser.positionalArguments(); + QString filePath = + positionalArgs.isEmpty() ? QString() : positionalArgs.first(); - // Create QML engine - QQmlApplicationEngine engine; + // Create QML engine + QQmlApplicationEngine engine; - // Expose application settings to QML - QQmlContext* rootContext = engine.rootContext(); - rootContext->setContextProperty("backendUrl", backendUrl); - rootContext->setContextProperty("standalone", standalone); - rootContext->setContextProperty("initialFile", filePath); + // Expose application settings to QML + QQmlContext *rootContext = engine.rootContext(); + rootContext->setContextProperty("backendUrl", backendUrl); + rootContext->setContextProperty("standalone", standalone); + rootContext->setContextProperty("initialFile", filePath); - // Load main QML file - const QUrl url(u"qrc:/qt/qml/WizardMerge/main.qml"_qs); - - QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, - &app, []() { - std::cerr << "Error: Failed to load QML" << std::endl; - QCoreApplication::exit(-1); - }, - Qt::QueuedConnection); + // Load main QML file + const QUrl url(u"qrc:/qt/qml/WizardMerge/main.qml"_qs); - engine.load(url); + QObject::connect( + &engine, &QQmlApplicationEngine::objectCreationFailed, &app, + []() { + std::cerr << "Error: Failed to load QML" << std::endl; + QCoreApplication::exit(-1); + }, + Qt::QueuedConnection); - if (engine.rootObjects().isEmpty()) { - std::cerr << "Error: No root objects loaded from QML" << std::endl; - return -1; - } + engine.load(url); - std::cout << "WizardMerge Qt6 Frontend Started" << std::endl; - std::cout << "Backend URL: " << backendUrl.toStdString() << std::endl; - std::cout << "Standalone Mode: " << (standalone ? "Yes" : "No") << std::endl; - if (!filePath.isEmpty()) { - std::cout << "Opening file: " << filePath.toStdString() << std::endl; - } + if (engine.rootObjects().isEmpty()) { + std::cerr << "Error: No root objects loaded from QML" << std::endl; + return -1; + } - return app.exec(); + std::cout << "WizardMerge Qt6 Frontend Started" << std::endl; + std::cout << "Backend URL: " << backendUrl.toStdString() << std::endl; + std::cout << "Standalone Mode: " << (standalone ? "Yes" : "No") << std::endl; + if (!filePath.isEmpty()) { + std::cout << "Opening file: " << filePath.toStdString() << std::endl; + } + + return app.exec(); }