9 Commits

Author SHA1 Message Date
12863cad56 Add GitHub Actions workflow for repository mirroring 2026-01-16 22:11:21 +00:00
1367ce54d0 Merge pull request #28 from johndoe6345789/copilot/run-cpp-lint-workflow-locally
Fix C++ formatting violations in backend and frontend code
2025-12-29 18:51:48 +00:00
copilot-swe-agent[bot]
cd2456db7c Fix C++ formatting issues with clang-format
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2025-12-29 18:45:12 +00:00
copilot-swe-agent[bot]
bc9f7663a4 Initial plan 2025-12-29 18:41:31 +00:00
217b8fce97 Merge pull request #27 from johndoe6345789/copilot/fix-clang-format-issues
Fix clang-format violations in file_utils.{cpp,h}
2025-12-29 18:40:26 +00:00
copilot-swe-agent[bot]
206451a997 Fix clang-format violations in file_utils files
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2025-12-29 18:34:41 +00:00
copilot-swe-agent[bot]
ceb9700239 Initial plan 2025-12-29 18:31:36 +00:00
3571cba0ea Merge pull request #26 from johndoe6345789/codex/run-act-on-branch-and-fix-issues
Fix tlaplus script lint warnings
2025-12-27 04:27:09 +00:00
ac0e2bc350 Fix tlaplus script lint warnings 2025-12-27 04:26:57 +00:00
27 changed files with 2973 additions and 3071 deletions

26
.github/workflows/mirror.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: mirror-repository
on:
push:
branches:
- '**'
workflow_dispatch:
jobs:
mirror:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Mirror repository
uses: yesolutions/mirror-action@v0.7.0
with:
REMOTE_NAME: git
REMOTE: https://git.wardcrew.com/git/wizardmerge.git
GIT_USERNAME: git
GIT_PASSWORD: 4wHhnUX7n7pVaFZi
PUSH_ALL_REFS: true
GIT_PUSH_ARGS: --tags --force --prune

View File

@@ -6,8 +6,8 @@
#include "wizardmerge/analysis/context_analyzer.h" #include "wizardmerge/analysis/context_analyzer.h"
#include "wizardmerge/analysis/risk_analyzer.h" #include "wizardmerge/analysis/risk_analyzer.h"
#include <iostream> #include <iostream>
#include <vector>
#include <string> #include <string>
#include <vector>
using namespace wizardmerge::analysis; using namespace wizardmerge::analysis;
@@ -26,9 +26,7 @@ int main() {
std::vector<std::string> ts_functions = { std::vector<std::string> ts_functions = {
"export async function fetchUser(id: number): Promise<User> {", "export async function fetchUser(id: number): Promise<User> {",
" const response = await api.get(`/users/${id}`);", " const response = await api.get(`/users/${id}`);",
" return response.data;", " return response.data;", "}"};
"}"
};
std::string func_name = extract_function_name(ts_functions, 1); std::string func_name = extract_function_name(ts_functions, 1);
std::cout << "Detected function: " << func_name << std::endl; std::cout << "Detected function: " << func_name << std::endl;
@@ -39,12 +37,8 @@ int main() {
std::cout << std::string(40, '-') << std::endl; std::cout << std::string(40, '-') << std::endl;
std::vector<std::string> ts_interface = { std::vector<std::string> ts_interface = {
"export interface User {", "export interface User {", " id: number;", " name: string;",
" id: number;", " email: string;", "}"};
" name: string;",
" email: string;",
"}"
};
std::string type_name = extract_class_name(ts_interface, 2); std::string type_name = extract_class_name(ts_interface, 2);
std::cout << "Detected type: " << type_name << std::endl; std::cout << "Detected type: " << type_name << std::endl;
@@ -57,10 +51,8 @@ int main() {
std::vector<std::string> ts_imports = { std::vector<std::string> ts_imports = {
"import { Component, useState } from 'react';", "import { Component, useState } from 'react';",
"import type { User } from './types';", "import type { User } from './types';",
"import * as utils from './utils';", "import * as utils from './utils';", "",
"", "export const MyComponent = () => {"};
"export const MyComponent = () => {"
};
auto imports = extract_imports(ts_imports); auto imports = extract_imports(ts_imports);
std::cout << "Detected " << imports.size() << " imports:" << std::endl; std::cout << "Detected " << imports.size() << " imports:" << std::endl;
@@ -74,11 +66,7 @@ int main() {
std::cout << std::string(40, '-') << std::endl; std::cout << std::string(40, '-') << std::endl;
std::vector<std::string> base_interface = { std::vector<std::string> base_interface = {
"interface User {", "interface User {", " id: number;", " name: string;", "}"};
" id: number;",
" name: string;",
"}"
};
std::vector<std::string> modified_interface = { std::vector<std::string> modified_interface = {
"interface User {", "interface User {",
@@ -86,12 +74,14 @@ int main() {
" name: string;", " name: string;",
" email: string; // Added", " email: string; // Added",
" age?: number; // Added optional", " age?: number; // Added optional",
"}" "}"};
};
bool has_ts_changes = has_typescript_interface_changes(base_interface, modified_interface); bool has_ts_changes =
std::cout << "Interface changed: " << (has_ts_changes ? "YES" : "NO") << std::endl; has_typescript_interface_changes(base_interface, modified_interface);
std::cout << "Risk: Breaking change - affects all usages of User" << std::endl; 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(); print_separator();
// Example 5: TypeScript Critical Pattern Detection // Example 5: TypeScript Critical Pattern Detection
@@ -107,11 +97,11 @@ int main() {
"element.innerHTML = userInput;", "element.innerHTML = userInput;",
"", "",
"// Insecure storage", "// Insecure storage",
"localStorage.setItem('password', pwd);" "localStorage.setItem('password', pwd);"};
};
bool has_critical = contains_critical_patterns(risky_code); bool has_critical = contains_critical_patterns(risky_code);
std::cout << "Contains critical patterns: " << (has_critical ? "YES" : "NO") << std::endl; std::cout << "Contains critical patterns: " << (has_critical ? "YES" : "NO")
<< std::endl;
if (has_critical) { if (has_critical) {
std::cout << "Critical issues detected:" << std::endl; std::cout << "Critical issues detected:" << std::endl;
std::cout << " - Type safety bypass (as any)" << std::endl; std::cout << " - Type safety bypass (as any)" << std::endl;
@@ -125,17 +115,14 @@ int main() {
std::cout << "Example 6: Package Lock File Detection" << std::endl; std::cout << "Example 6: Package Lock File Detection" << std::endl;
std::cout << std::string(40, '-') << std::endl; std::cout << std::string(40, '-') << std::endl;
std::vector<std::string> lock_files = { std::vector<std::string> lock_files = {"package-lock.json", "yarn.lock",
"package-lock.json", "pnpm-lock.yaml", "bun.lockb",
"yarn.lock", "package.json"};
"pnpm-lock.yaml",
"bun.lockb",
"package.json"
};
for (const auto &file : lock_files) { for (const auto &file : lock_files) {
bool is_lock = is_package_lock_file(file); bool is_lock = is_package_lock_file(file);
std::cout << file << ": " << (is_lock ? "LOCK FILE" : "regular file") << std::endl; std::cout << file << ": " << (is_lock ? "LOCK FILE" : "regular file")
<< std::endl;
} }
std::cout << "\nRecommendation for lock file conflicts:" << std::endl; std::cout << "\nRecommendation for lock file conflicts:" << std::endl;
@@ -145,33 +132,25 @@ int main() {
print_separator(); print_separator();
// Example 7: Complete Risk Analysis // Example 7: Complete Risk Analysis
std::cout << "Example 7: Complete Risk Analysis for TypeScript Changes" << std::endl; std::cout << "Example 7: Complete Risk Analysis for TypeScript Changes"
<< std::endl;
std::cout << std::string(40, '-') << std::endl; std::cout << std::string(40, '-') << std::endl;
std::vector<std::string> base = { std::vector<std::string> base = {"interface Config {", " timeout: number;",
"interface Config {", "}"};
" timeout: number;",
"}"
};
std::vector<std::string> ours = { std::vector<std::string> ours = {"interface Config {", " timeout: number;",
"interface Config {", " retries: number;", "}"};
" timeout: number;",
" retries: number;",
"}"
};
std::vector<std::string> theirs = { std::vector<std::string> theirs = {"interface Config {",
"interface Config {", " timeout: number;", "}"};
" timeout: number;",
"}"
};
auto risk = analyze_risk_ours(base, ours, theirs); auto risk = analyze_risk_ours(base, ours, theirs);
std::cout << "Risk Level: " << risk_level_to_string(risk.level) << std::endl; std::cout << "Risk Level: " << risk_level_to_string(risk.level) << std::endl;
std::cout << "Confidence: " << risk.confidence_score << 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 << "Has API Changes: " << (risk.has_api_changes ? "YES" : "NO")
<< std::endl;
std::cout << "\nRisk Factors:" << std::endl; std::cout << "\nRisk Factors:" << std::endl;
for (const auto &factor : risk.risk_factors) { for (const auto &factor : risk.risk_factors) {

View File

@@ -9,9 +9,9 @@
#ifndef WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H #ifndef WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H
#define WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H #define WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H
#include <map>
#include <string> #include <string>
#include <vector> #include <vector>
#include <map>
namespace wizardmerge { namespace wizardmerge {
namespace analysis { namespace analysis {
@@ -42,12 +42,9 @@ struct CodeContext {
* @param context_window Number of lines before/after to include (default: 5) * @param context_window Number of lines before/after to include (default: 5)
* @return CodeContext containing analyzed context information * @return CodeContext containing analyzed context information
*/ */
CodeContext analyze_context( CodeContext analyze_context(const std::vector<std::string> &lines,
const std::vector<std::string>& lines, size_t start_line, size_t end_line,
size_t start_line, size_t context_window = 5);
size_t end_line,
size_t context_window = 5
);
/** /**
* @brief Extracts function or method name from context. * @brief Extracts function or method name from context.
@@ -59,10 +56,8 @@ CodeContext analyze_context(
* @param line_number Line number to check * @param line_number Line number to check
* @return Function name if found, empty string otherwise * @return Function name if found, empty string otherwise
*/ */
std::string extract_function_name( std::string extract_function_name(const std::vector<std::string> &lines,
const std::vector<std::string>& lines, size_t line_number);
size_t line_number
);
/** /**
* @brief Extracts class name from context. * @brief Extracts class name from context.
@@ -74,10 +69,8 @@ std::string extract_function_name(
* @param line_number Line number to check * @param line_number Line number to check
* @return Class name if found, empty string otherwise * @return Class name if found, empty string otherwise
*/ */
std::string extract_class_name( std::string extract_class_name(const std::vector<std::string> &lines,
const std::vector<std::string>& lines, size_t line_number);
size_t line_number
);
/** /**
* @brief Extracts import/include statements from the file. * @brief Extracts import/include statements from the file.
@@ -88,9 +81,7 @@ std::string extract_class_name(
* @param lines Lines of code to analyze * @param lines Lines of code to analyze
* @return Vector of import statements * @return Vector of import statements
*/ */
std::vector<std::string> extract_imports( std::vector<std::string> extract_imports(const std::vector<std::string> &lines);
const std::vector<std::string>& lines
);
} // namespace analysis } // namespace analysis
} // namespace wizardmerge } // namespace wizardmerge

View File

@@ -50,11 +50,9 @@ struct RiskAssessment {
* @param theirs Their version lines * @param theirs Their version lines
* @return RiskAssessment for accepting ours * @return RiskAssessment for accepting ours
*/ */
RiskAssessment analyze_risk_ours( RiskAssessment analyze_risk_ours(const std::vector<std::string> &base,
const std::vector<std::string>& base,
const std::vector<std::string> &ours, const std::vector<std::string> &ours,
const std::vector<std::string>& theirs const std::vector<std::string> &theirs);
);
/** /**
* @brief Analyzes risk of accepting "theirs" version. * @brief Analyzes risk of accepting "theirs" version.
@@ -64,11 +62,9 @@ RiskAssessment analyze_risk_ours(
* @param theirs Their version lines * @param theirs Their version lines
* @return RiskAssessment for accepting theirs * @return RiskAssessment for accepting theirs
*/ */
RiskAssessment analyze_risk_theirs( RiskAssessment analyze_risk_theirs(const std::vector<std::string> &base,
const std::vector<std::string>& base,
const std::vector<std::string> &ours, const std::vector<std::string> &ours,
const std::vector<std::string>& theirs const std::vector<std::string> &theirs);
);
/** /**
* @brief Analyzes risk of accepting both versions (concatenation). * @brief Analyzes risk of accepting both versions (concatenation).
@@ -78,11 +74,9 @@ RiskAssessment analyze_risk_theirs(
* @param theirs Their version lines * @param theirs Their version lines
* @return RiskAssessment for accepting both * @return RiskAssessment for accepting both
*/ */
RiskAssessment analyze_risk_both( RiskAssessment analyze_risk_both(const std::vector<std::string> &base,
const std::vector<std::string>& base,
const std::vector<std::string> &ours, const std::vector<std::string> &ours,
const std::vector<std::string>& theirs const std::vector<std::string> &theirs);
);
/** /**
* @brief Converts RiskLevel to string representation. * @brief Converts RiskLevel to string representation.
@@ -107,10 +101,8 @@ bool contains_critical_patterns(const std::vector<std::string>& lines);
* @param modified Modified version lines * @param modified Modified version lines
* @return true if API changes detected * @return true if API changes detected
*/ */
bool has_api_signature_changes( bool has_api_signature_changes(const std::vector<std::string> &base,
const std::vector<std::string>& base, const std::vector<std::string> &modified);
const std::vector<std::string>& modified
);
/** /**
* @brief Detects if TypeScript interface or type definitions changed. * @brief Detects if TypeScript interface or type definitions changed.
@@ -119,10 +111,8 @@ bool has_api_signature_changes(
* @param modified Modified version lines * @param modified Modified version lines
* @return true if interface/type changes detected * @return true if interface/type changes detected
*/ */
bool has_typescript_interface_changes( bool has_typescript_interface_changes(const std::vector<std::string> &base,
const std::vector<std::string>& base, const std::vector<std::string> &modified);
const std::vector<std::string>& modified
);
/** /**
* @brief Checks if file is a package-lock.json file. * @brief Checks if file is a package-lock.json file.

View File

@@ -9,9 +9,9 @@
#ifndef WIZARDMERGE_GIT_CLI_H #ifndef WIZARDMERGE_GIT_CLI_H
#define WIZARDMERGE_GIT_CLI_H #define WIZARDMERGE_GIT_CLI_H
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
#include <optional>
namespace wizardmerge { namespace wizardmerge {
namespace git { namespace git {
@@ -44,12 +44,9 @@ struct GitConfig {
* @param depth Optional shallow clone depth (0 for full clone) * @param depth Optional shallow clone depth (0 for full clone)
* @return GitResult with operation status * @return GitResult with operation status
*/ */
GitResult clone_repository( GitResult clone_repository(const std::string &url,
const std::string& url,
const std::string &destination, const std::string &destination,
const std::string& branch = "", const std::string &branch = "", int depth = 0);
int depth = 0
);
/** /**
* @brief Create and checkout a new branch * @brief Create and checkout a new branch
@@ -59,11 +56,9 @@ GitResult clone_repository(
* @param base_branch Optional base branch (defaults to current branch) * @param base_branch Optional base branch (defaults to current branch)
* @return GitResult with operation status * @return GitResult with operation status
*/ */
GitResult create_branch( GitResult create_branch(const std::string &repo_path,
const std::string& repo_path,
const std::string &branch_name, const std::string &branch_name,
const std::string& base_branch = "" const std::string &base_branch = "");
);
/** /**
* @brief Checkout an existing branch * @brief Checkout an existing branch
@@ -72,10 +67,8 @@ GitResult create_branch(
* @param branch_name Name of the branch to checkout * @param branch_name Name of the branch to checkout
* @return GitResult with operation status * @return GitResult with operation status
*/ */
GitResult checkout_branch( GitResult checkout_branch(const std::string &repo_path,
const std::string& repo_path, const std::string &branch_name);
const std::string& branch_name
);
/** /**
* @brief Stage files for commit * @brief Stage files for commit
@@ -84,10 +77,8 @@ GitResult checkout_branch(
* @param files Vector of file paths (relative to repo root) * @param files Vector of file paths (relative to repo root)
* @return GitResult with operation status * @return GitResult with operation status
*/ */
GitResult add_files( GitResult add_files(const std::string &repo_path,
const std::string& repo_path, const std::vector<std::string> &files);
const std::vector<std::string>& files
);
/** /**
* @brief Commit staged changes * @brief Commit staged changes
@@ -97,11 +88,8 @@ GitResult add_files(
* @param config Optional Git configuration * @param config Optional Git configuration
* @return GitResult with operation status * @return GitResult with operation status
*/ */
GitResult commit( GitResult commit(const std::string &repo_path, const std::string &message,
const std::string& repo_path, const GitConfig &config = GitConfig());
const std::string& message,
const GitConfig& config = GitConfig()
);
/** /**
* @brief Push commits to remote repository * @brief Push commits to remote repository
@@ -113,13 +101,9 @@ GitResult commit(
* @param config Optional Git configuration with auth token * @param config Optional Git configuration with auth token
* @return GitResult with operation status * @return GitResult with operation status
*/ */
GitResult push( GitResult push(const std::string &repo_path, const std::string &remote,
const std::string& repo_path, const std::string &branch, bool force = false,
const std::string& remote, const GitConfig &config = GitConfig());
const std::string& branch,
bool force = false,
const GitConfig& config = GitConfig()
);
/** /**
* @brief Get current branch name * @brief Get current branch name
@@ -136,7 +120,8 @@ std::optional<std::string> get_current_branch(const std::string& repo_path);
* @param branch_name Name of the branch to check * @param branch_name Name of the branch to check
* @return true if branch exists, false otherwise * @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 * @brief Get repository status

View File

@@ -8,10 +8,10 @@
#ifndef WIZARDMERGE_GIT_PLATFORM_CLIENT_H #ifndef WIZARDMERGE_GIT_PLATFORM_CLIENT_H
#define WIZARDMERGE_GIT_PLATFORM_CLIENT_H #define WIZARDMERGE_GIT_PLATFORM_CLIENT_H
#include <string>
#include <vector>
#include <map> #include <map>
#include <optional> #include <optional>
#include <string>
#include <vector>
namespace wizardmerge { namespace wizardmerge {
namespace git { namespace git {
@@ -19,11 +19,7 @@ namespace git {
/** /**
* @brief Supported git platforms * @brief Supported git platforms
*/ */
enum class GitPlatform { enum class GitPlatform { GitHub, GitLab, Unknown };
GitHub,
GitLab,
Unknown
};
/** /**
* @brief Information about a file in a pull/merge request * @brief Information about a file in a pull/merge request
@@ -84,13 +80,11 @@ bool parse_pr_url(const std::string& url, GitPlatform& platform,
* @param token Optional API token for authentication * @param token Optional API token for authentication
* @return Pull request information, or empty optional on error * @return Pull request information, or empty optional on error
*/ */
std::optional<PullRequest> fetch_pull_request( std::optional<PullRequest> fetch_pull_request(GitPlatform platform,
GitPlatform platform,
const std::string &owner, const std::string &owner,
const std::string &repo, const std::string &repo,
int pr_number, int pr_number,
const std::string& token = "" const std::string &token = "");
);
/** /**
* @brief Fetch file content from GitHub or GitLab at a specific commit * @brief Fetch file content from GitHub or GitLab at a specific commit
@@ -103,14 +97,10 @@ std::optional<PullRequest> fetch_pull_request(
* @param token Optional API token * @param token Optional API token
* @return File content as vector of lines, or empty optional on error * @return File content as vector of lines, or empty optional on error
*/ */
std::optional<std::vector<std::string>> fetch_file_content( std::optional<std::vector<std::string>>
GitPlatform platform, fetch_file_content(GitPlatform platform, const std::string &owner,
const std::string& owner, const std::string &repo, const std::string &sha,
const std::string& repo, const std::string &path, const std::string &token = "");
const std::string& sha,
const std::string& path,
const std::string& token = ""
);
} // namespace git } // namespace git
} // namespace wizardmerge } // namespace wizardmerge

View File

@@ -10,10 +10,10 @@
#ifndef WIZARDMERGE_MERGE_THREE_WAY_MERGE_H #ifndef WIZARDMERGE_MERGE_THREE_WAY_MERGE_H
#define WIZARDMERGE_MERGE_THREE_WAY_MERGE_H #define WIZARDMERGE_MERGE_THREE_WAY_MERGE_H
#include <string>
#include <vector>
#include "wizardmerge/analysis/context_analyzer.h" #include "wizardmerge/analysis/context_analyzer.h"
#include "wizardmerge/analysis/risk_analyzer.h" #include "wizardmerge/analysis/risk_analyzer.h"
#include <string>
#include <vector>
namespace wizardmerge { namespace wizardmerge {
namespace merge { namespace merge {
@@ -65,11 +65,9 @@ struct MergeResult {
* @param theirs Their version (branch being merged) * @param theirs Their version (branch being merged)
* @return MergeResult containing the merged content and any conflicts * @return MergeResult containing the merged content and any conflicts
*/ */
MergeResult three_way_merge( MergeResult three_way_merge(const std::vector<std::string> &base,
const std::vector<std::string>& base,
const std::vector<std::string> &ours, const std::vector<std::string> &ours,
const std::vector<std::string>& theirs const std::vector<std::string> &theirs);
);
/** /**
* @brief Auto-resolves simple non-conflicting patterns. * @brief Auto-resolves simple non-conflicting patterns.

View File

@@ -21,7 +21,8 @@ constexpr size_t IMPORT_SCAN_LIMIT = 50;
std::string trim(const std::string &str) { std::string trim(const std::string &str) {
size_t start = str.find_first_not_of(" \t\n\r"); size_t start = str.find_first_not_of(" \t\n\r");
size_t end = str.find_last_not_of(" \t\n\r"); size_t end = str.find_last_not_of(" \t\n\r");
if (start == std::string::npos) return ""; if (start == std::string::npos)
return "";
return str.substr(start, end - start + 1); return str.substr(start, end - start + 1);
} }
@@ -33,15 +34,23 @@ bool is_function_definition(const std::string& line) {
// Common function patterns across languages // Common function patterns across languages
std::vector<std::regex> patterns = { std::vector<std::regex> patterns = {
std::regex(R"(^\w+\s+\w+\s*\([^)]*\)\s*\{?)"), // C/C++/Java: type name(params) 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"(^def\s+\w+\s*\([^)]*\):)"), // Python: def name(params):
std::regex(R"(^function\s+\w+\s*\([^)]*\))"), // JavaScript: function 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"(^\w+\s*:\s*function\s*\([^)]*\))"), // JS object method
std::regex(R"(^(public|private|protected)?\s*\w+\s+\w+\s*\([^)]*\))"), // Java/C# methods std::regex(
R"(^(public|private|protected)?\s*\w+\s+\w+\s*\([^)]*\))"), // Java/C#
// methods
// TypeScript patterns // TypeScript patterns
std::regex(R"(^(export\s+)?(async\s+)?function\s+\w+)"), // TS: export/async function std::regex(
std::regex(R"(^(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>)"), // TS: arrow functions R"(^(export\s+)?(async\s+)?function\s+\w+)"), // TS: export/async
std::regex(R"(^(public|private|protected|readonly)?\s*\w+\s*\([^)]*\)\s*:\s*\w+)") // TS: typed methods // 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) { for (const auto &pattern : patterns) {
@@ -68,14 +77,16 @@ std::string get_function_name_from_line(const std::string& line) {
return match[1].str(); return match[1].str();
} }
// JavaScript/TypeScript: function function_name( or export function function_name( // JavaScript/TypeScript: function function_name( or export function
// function_name(
std::regex js_pattern(R"((?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\()"); std::regex js_pattern(R"((?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\()");
if (std::regex_search(trimmed, match, js_pattern)) { if (std::regex_search(trimmed, match, js_pattern)) {
return match[1].str(); return match[1].str();
} }
// TypeScript: const/let/var function_name = (params) => // TypeScript: const/let/var function_name = (params) =>
std::regex arrow_pattern(R"((?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)"); std::regex arrow_pattern(
R"((?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)");
if (std::regex_search(trimmed, match, arrow_pattern)) { if (std::regex_search(trimmed, match, arrow_pattern)) {
return match[1].str(); return match[1].str();
} }
@@ -97,10 +108,12 @@ bool is_class_definition(const std::string& line) {
std::vector<std::regex> patterns = { std::vector<std::regex> patterns = {
std::regex(R"(^class\s+\w+)"), // Python/C++/Java: class Name 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"(^(public|private)?\s*class\s+\w+)"), // Java/C#: visibility
// class Name
std::regex(R"(^struct\s+\w+)"), // C/C++: struct Name std::regex(R"(^struct\s+\w+)"), // C/C++: struct Name
// TypeScript patterns // TypeScript patterns
std::regex(R"(^(export\s+)?(abstract\s+)?class\s+\w+)"), // TS: export class Name 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+)?interface\s+\w+)"), // TS: interface Name
std::regex(R"(^(export\s+)?type\s+\w+\s*=)"), // TS: type Name = std::regex(R"(^(export\s+)?type\s+\w+\s*=)"), // TS: type Name =
std::regex(R"(^(export\s+)?enum\s+\w+)") // TS: enum Name std::regex(R"(^(export\s+)?enum\s+\w+)") // TS: enum Name
@@ -124,7 +137,8 @@ std::string get_class_name_from_line(const std::string& line) {
std::smatch match; std::smatch match;
// Match class, struct, interface, type, or enum // Match class, struct, interface, type, or enum
std::regex pattern(R"((?:export\s+)?(?:abstract\s+)?(class|struct|interface|type|enum)\s+(\w+))"); std::regex pattern(
R"((?:export\s+)?(?:abstract\s+)?(class|struct|interface|type|enum)\s+(\w+))");
if (std::regex_search(trimmed, match, pattern)) { if (std::regex_search(trimmed, match, pattern)) {
return match[2].str(); return match[2].str();
@@ -135,18 +149,16 @@ std::string get_class_name_from_line(const std::string& line) {
} // anonymous namespace } // anonymous namespace
CodeContext analyze_context( CodeContext analyze_context(const std::vector<std::string> &lines,
const std::vector<std::string>& lines, size_t start_line, size_t end_line,
size_t start_line, size_t context_window) {
size_t end_line,
size_t context_window
) {
CodeContext context; CodeContext context;
context.start_line = start_line; context.start_line = start_line;
context.end_line = end_line; context.end_line = end_line;
// Extract surrounding lines // Extract surrounding lines
size_t window_start = (start_line >= context_window) ? (start_line - context_window) : 0; 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()); size_t window_end = std::min(end_line + context_window, lines.size());
for (size_t i = window_start; i < window_end; ++i) { for (size_t i = window_start; i < window_end; ++i) {
@@ -170,10 +182,8 @@ CodeContext analyze_context(
return context; return context;
} }
std::string extract_function_name( std::string extract_function_name(const std::vector<std::string> &lines,
const std::vector<std::string>& lines, size_t line_number) {
size_t line_number
) {
if (line_number >= lines.size()) { if (line_number >= lines.size()) {
return ""; return "";
} }
@@ -199,10 +209,8 @@ std::string extract_function_name(
return ""; return "";
} }
std::string extract_class_name( std::string extract_class_name(const std::vector<std::string> &lines,
const std::vector<std::string>& lines, size_t line_number) {
size_t line_number
) {
if (line_number >= lines.size()) { if (line_number >= lines.size()) {
return ""; return "";
} }
@@ -224,9 +232,8 @@ std::string extract_class_name(
return ""; return "";
} }
std::vector<std::string> extract_imports( std::vector<std::string>
const std::vector<std::string>& lines extract_imports(const std::vector<std::string> &lines) {
) {
std::vector<std::string> imports; std::vector<std::string> imports;
// Scan first lines for imports (imports are typically at the top) // Scan first lines for imports (imports are typically at the top)
@@ -236,17 +243,13 @@ std::vector<std::string> extract_imports(
std::string line = trim(lines[i]); std::string line = trim(lines[i]);
// Check for various import patterns // Check for various import patterns
if (line.find("#include") == 0 || if (line.find("#include") == 0 || line.find("import ") == 0 ||
line.find("import ") == 0 ||
line.find("import{") == 0 || // Support both "import{" and "import {" line.find("import{") == 0 || // Support both "import{" and "import {"
line.find("from ") == 0 || line.find("from ") == 0 || line.find("require(") != std::string::npos ||
line.find("require(") != std::string::npos ||
line.find("using ") == 0 || line.find("using ") == 0 ||
// TypeScript/ES6 specific patterns // TypeScript/ES6 specific patterns
line.find("import *") == 0 || line.find("import *") == 0 || line.find("import type") == 0 ||
line.find("import type") == 0 || line.find("export {") == 0 || line.find("export *") == 0) {
line.find("export {") == 0 ||
line.find("export *") == 0) {
imports.push_back(line); imports.push_back(line);
} }
} }

View File

@@ -5,8 +5,8 @@
#include "wizardmerge/analysis/risk_analyzer.h" #include "wizardmerge/analysis/risk_analyzer.h"
#include <algorithm> #include <algorithm>
#include <regex>
#include <cmath> #include <cmath>
#include <regex>
namespace wizardmerge { namespace wizardmerge {
namespace analysis { namespace analysis {
@@ -24,19 +24,20 @@ constexpr double CHANGE_RATIO_WEIGHT = 0.2; // Weight for change ratio
std::string trim(const std::string &str) { std::string trim(const std::string &str) {
size_t start = str.find_first_not_of(" \t\n\r"); size_t start = str.find_first_not_of(" \t\n\r");
size_t end = str.find_last_not_of(" \t\n\r"); size_t end = str.find_last_not_of(" \t\n\r");
if (start == std::string::npos) return ""; if (start == std::string::npos)
return "";
return str.substr(start, end - start + 1); return str.substr(start, end - start + 1);
} }
/** /**
* @brief Calculate similarity score between two sets of lines (0.0 to 1.0). * @brief Calculate similarity score between two sets of lines (0.0 to 1.0).
*/ */
double calculate_similarity( double calculate_similarity(const std::vector<std::string> &lines1,
const std::vector<std::string>& lines1, const std::vector<std::string> &lines2) {
const std::vector<std::string>& lines2 if (lines1.empty() && lines2.empty())
) { return 1.0;
if (lines1.empty() && lines2.empty()) return 1.0; if (lines1.empty() || lines2.empty())
if (lines1.empty() || lines2.empty()) return 0.0; return 0.0;
// Simple Jaccard similarity on lines // Simple Jaccard similarity on lines
size_t common_lines = 0; size_t common_lines = 0;
@@ -47,16 +48,15 @@ double calculate_similarity(
} }
size_t total_unique = lines1.size() + lines2.size() - common_lines; size_t total_unique = lines1.size() + lines2.size() - common_lines;
return total_unique > 0 ? static_cast<double>(common_lines) / total_unique : 0.0; return total_unique > 0 ? static_cast<double>(common_lines) / total_unique
: 0.0;
} }
/** /**
* @brief Count number of changed lines between two versions. * @brief Count number of changed lines between two versions.
*/ */
size_t count_changes( size_t count_changes(const std::vector<std::string> &base,
const std::vector<std::string>& base, const std::vector<std::string> &modified) {
const std::vector<std::string>& modified
) {
size_t changes = 0; size_t changes = 0;
size_t max_len = std::max(base.size(), modified.size()); size_t max_len = std::max(base.size(), modified.size());
@@ -83,9 +83,13 @@ bool is_function_signature(const std::string& line) {
std::regex(R"(^def\s+\w+\s*\([^)]*\):)"), // Python std::regex(R"(^def\s+\w+\s*\([^)]*\):)"), // Python
std::regex(R"(^function\s+\w+\s*\([^)]*\))"), // JavaScript std::regex(R"(^function\s+\w+\s*\([^)]*\))"), // JavaScript
// TypeScript patterns // TypeScript patterns
std::regex(R"(^(export\s+)?(async\s+)?function\s+\w+\s*\([^)]*\))"), // TS function std::regex(
std::regex(R"(^(const|let|var)\s+\w+\s*=\s*\([^)]*\)\s*=>)"), // Arrow function R"(^(export\s+)?(async\s+)?function\s+\w+\s*\([^)]*\))"), // TS
std::regex(R"(^\w+\s*\([^)]*\)\s*:\s*\w+)"), // TS: method with return type // 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) { for (const auto &pattern : patterns) {
@@ -101,11 +105,16 @@ bool is_function_signature(const std::string& line) {
std::string risk_level_to_string(RiskLevel level) { std::string risk_level_to_string(RiskLevel level) {
switch (level) { switch (level) {
case RiskLevel::LOW: return "low"; case RiskLevel::LOW:
case RiskLevel::MEDIUM: return "medium"; return "low";
case RiskLevel::HIGH: return "high"; case RiskLevel::MEDIUM:
case RiskLevel::CRITICAL: return "critical"; return "medium";
default: return "unknown"; case RiskLevel::HIGH:
return "high";
case RiskLevel::CRITICAL:
return "critical";
default:
return "unknown";
} }
} }
@@ -126,7 +135,8 @@ bool contains_critical_patterns(const std::vector<std::string>& lines) {
std::regex(R"(\bas\s+any\b)"), // TypeScript: type safety bypass std::regex(R"(\bas\s+any\b)"), // TypeScript: type safety bypass
std::regex(R"(@ts-ignore)"), // TypeScript: error suppression std::regex(R"(@ts-ignore)"), // TypeScript: error suppression
std::regex(R"(@ts-nocheck)"), // TypeScript: file-level 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"(localStorage\.setItem.*password)"), // Storing passwords in
// localStorage
std::regex(R"(innerHTML\s*=)"), // XSS risk std::regex(R"(innerHTML\s*=)"), // XSS risk
}; };
@@ -142,10 +152,8 @@ bool contains_critical_patterns(const std::vector<std::string>& lines) {
return false; return false;
} }
bool has_api_signature_changes( bool has_api_signature_changes(const std::vector<std::string> &base,
const std::vector<std::string>& base, const std::vector<std::string> &modified) {
const std::vector<std::string>& modified
) {
// Check if function signatures changed // Check if function signatures changed
for (size_t i = 0; i < base.size() && i < modified.size(); ++i) { for (size_t i = 0; i < base.size() && i < modified.size(); ++i) {
bool base_is_sig = is_function_signature(base[i]); bool base_is_sig = is_function_signature(base[i]);
@@ -161,8 +169,7 @@ bool has_api_signature_changes(
bool has_typescript_interface_changes( bool has_typescript_interface_changes(
const std::vector<std::string> &base, const std::vector<std::string> &base,
const std::vector<std::string>& modified const std::vector<std::string> &modified) {
) {
// Use static regex patterns to avoid recompilation // Use static regex patterns to avoid recompilation
static const std::vector<std::regex> ts_definition_patterns = { static const std::vector<std::regex> ts_definition_patterns = {
std::regex(R"(\binterface\s+\w+)"), std::regex(R"(\binterface\s+\w+)"),
@@ -180,7 +187,8 @@ bool has_typescript_interface_changes(
break; break;
} }
} }
if (base_has_ts_def) break; if (base_has_ts_def)
break;
} }
// Check if any TypeScript definition exists in modified // Check if any TypeScript definition exists in modified
@@ -193,7 +201,8 @@ bool has_typescript_interface_changes(
break; break;
} }
} }
if (modified_has_ts_def) break; if (modified_has_ts_def)
break;
} }
// If either has TS definitions and content differs, it's a TS change // If either has TS definitions and content differs, it's a TS change
@@ -223,11 +232,9 @@ bool is_package_lock_file(const std::string& filename) {
filename.find("bun.lockb") != std::string::npos; filename.find("bun.lockb") != std::string::npos;
} }
RiskAssessment analyze_risk_ours( RiskAssessment analyze_risk_ours(const std::vector<std::string> &base,
const std::vector<std::string>& base,
const std::vector<std::string> &ours, const std::vector<std::string> &ours,
const std::vector<std::string>& theirs const std::vector<std::string> &theirs) {
) {
RiskAssessment assessment; RiskAssessment assessment;
assessment.level = RiskLevel::LOW; assessment.level = RiskLevel::LOW;
assessment.confidence_score = 0.5; assessment.confidence_score = 0.5;
@@ -245,7 +252,8 @@ RiskAssessment analyze_risk_ours(
// Check for critical patterns // Check for critical patterns
if (contains_critical_patterns(ours)) { if (contains_critical_patterns(ours)) {
assessment.affects_critical_section = true; assessment.affects_critical_section = true;
assessment.risk_factors.push_back("Contains critical code patterns (security/data operations)"); assessment.risk_factors.push_back(
"Contains critical code patterns (security/data operations)");
assessment.level = RiskLevel::HIGH; assessment.level = RiskLevel::HIGH;
} }
@@ -261,7 +269,8 @@ RiskAssessment analyze_risk_ours(
// Check for TypeScript interface/type changes // Check for TypeScript interface/type changes
if (has_typescript_interface_changes(base, ours)) { if (has_typescript_interface_changes(base, ours)) {
assessment.has_api_changes = true; assessment.has_api_changes = true;
assessment.risk_factors.push_back("TypeScript interface or type definitions changed"); assessment.risk_factors.push_back(
"TypeScript interface or type definitions changed");
if (assessment.level < RiskLevel::MEDIUM) { if (assessment.level < RiskLevel::MEDIUM) {
assessment.level = RiskLevel::MEDIUM; assessment.level = RiskLevel::MEDIUM;
} }
@@ -270,7 +279,8 @@ RiskAssessment analyze_risk_ours(
// Assess based on amount of change // Assess based on amount of change
if (our_changes > 10) { if (our_changes > 10) {
assessment.has_logic_changes = true; assessment.has_logic_changes = true;
assessment.risk_factors.push_back("Large number of changes (" + std::to_string(our_changes) + " lines)"); assessment.risk_factors.push_back("Large number of changes (" +
std::to_string(our_changes) + " lines)");
if (assessment.level < RiskLevel::MEDIUM) { if (assessment.level < RiskLevel::MEDIUM) {
assessment.level = RiskLevel::MEDIUM; assessment.level = RiskLevel::MEDIUM;
} }
@@ -278,7 +288,8 @@ RiskAssessment analyze_risk_ours(
// Check if we're discarding significant changes from theirs // Check if we're discarding significant changes from theirs
if (their_changes > 5 && similarity_to_theirs < 0.3) { if (their_changes > 5 && similarity_to_theirs < 0.3) {
assessment.risk_factors.push_back("Discarding significant changes from other branch (" + assessment.risk_factors.push_back(
"Discarding significant changes from other branch (" +
std::to_string(their_changes) + " lines)"); std::to_string(their_changes) + " lines)");
if (assessment.level < RiskLevel::MEDIUM) { if (assessment.level < RiskLevel::MEDIUM) {
assessment.level = RiskLevel::MEDIUM; assessment.level = RiskLevel::MEDIUM;
@@ -286,21 +297,26 @@ RiskAssessment analyze_risk_ours(
} }
// Calculate confidence score based on various factors // Calculate confidence score based on various factors
double change_ratio = (our_changes + their_changes) > 0 ? double change_ratio =
static_cast<double>(our_changes) / (our_changes + their_changes) : BASE_CONFIDENCE; (our_changes + their_changes) > 0
? static_cast<double>(our_changes) / (our_changes + their_changes)
: BASE_CONFIDENCE;
assessment.confidence_score = BASE_CONFIDENCE + assessment.confidence_score = BASE_CONFIDENCE +
(SIMILARITY_WEIGHT * similarity_to_theirs) + (SIMILARITY_WEIGHT * similarity_to_theirs) +
(CHANGE_RATIO_WEIGHT * change_ratio); (CHANGE_RATIO_WEIGHT * change_ratio);
// Add recommendations // Add recommendations
if (assessment.level >= RiskLevel::MEDIUM) { if (assessment.level >= RiskLevel::MEDIUM) {
assessment.recommendations.push_back("Review changes carefully before accepting"); assessment.recommendations.push_back(
"Review changes carefully before accepting");
} }
if (assessment.has_api_changes) { if (assessment.has_api_changes) {
assessment.recommendations.push_back("Verify API compatibility with dependent code"); assessment.recommendations.push_back(
"Verify API compatibility with dependent code");
} }
if (assessment.affects_critical_section) { if (assessment.affects_critical_section) {
assessment.recommendations.push_back("Test thoroughly, especially security and data operations"); assessment.recommendations.push_back(
"Test thoroughly, especially security and data operations");
} }
if (assessment.risk_factors.empty()) { if (assessment.risk_factors.empty()) {
assessment.recommendations.push_back("Changes appear safe to accept"); assessment.recommendations.push_back("Changes appear safe to accept");
@@ -309,11 +325,9 @@ RiskAssessment analyze_risk_ours(
return assessment; return assessment;
} }
RiskAssessment analyze_risk_theirs( RiskAssessment analyze_risk_theirs(const std::vector<std::string> &base,
const std::vector<std::string>& base,
const std::vector<std::string> &ours, const std::vector<std::string> &ours,
const std::vector<std::string>& theirs const std::vector<std::string> &theirs) {
) {
RiskAssessment assessment; RiskAssessment assessment;
assessment.level = RiskLevel::LOW; assessment.level = RiskLevel::LOW;
assessment.confidence_score = 0.5; assessment.confidence_score = 0.5;
@@ -331,7 +345,8 @@ RiskAssessment analyze_risk_theirs(
// Check for critical patterns // Check for critical patterns
if (contains_critical_patterns(theirs)) { if (contains_critical_patterns(theirs)) {
assessment.affects_critical_section = true; assessment.affects_critical_section = true;
assessment.risk_factors.push_back("Contains critical code patterns (security/data operations)"); assessment.risk_factors.push_back(
"Contains critical code patterns (security/data operations)");
assessment.level = RiskLevel::HIGH; assessment.level = RiskLevel::HIGH;
} }
@@ -347,7 +362,8 @@ RiskAssessment analyze_risk_theirs(
// Check for TypeScript interface/type changes // Check for TypeScript interface/type changes
if (has_typescript_interface_changes(base, theirs)) { if (has_typescript_interface_changes(base, theirs)) {
assessment.has_api_changes = true; assessment.has_api_changes = true;
assessment.risk_factors.push_back("TypeScript interface or type definitions changed"); assessment.risk_factors.push_back(
"TypeScript interface or type definitions changed");
if (assessment.level < RiskLevel::MEDIUM) { if (assessment.level < RiskLevel::MEDIUM) {
assessment.level = RiskLevel::MEDIUM; assessment.level = RiskLevel::MEDIUM;
} }
@@ -356,7 +372,9 @@ RiskAssessment analyze_risk_theirs(
// Assess based on amount of change // Assess based on amount of change
if (their_changes > 10) { if (their_changes > 10) {
assessment.has_logic_changes = true; assessment.has_logic_changes = true;
assessment.risk_factors.push_back("Large number of changes (" + std::to_string(their_changes) + " lines)"); assessment.risk_factors.push_back("Large number of changes (" +
std::to_string(their_changes) +
" lines)");
if (assessment.level < RiskLevel::MEDIUM) { if (assessment.level < RiskLevel::MEDIUM) {
assessment.level = RiskLevel::MEDIUM; assessment.level = RiskLevel::MEDIUM;
} }
@@ -372,21 +390,26 @@ RiskAssessment analyze_risk_theirs(
} }
// Calculate confidence score // Calculate confidence score
double change_ratio = (our_changes + their_changes) > 0 ? double change_ratio =
static_cast<double>(their_changes) / (our_changes + their_changes) : BASE_CONFIDENCE; (our_changes + their_changes) > 0
? static_cast<double>(their_changes) / (our_changes + their_changes)
: BASE_CONFIDENCE;
assessment.confidence_score = BASE_CONFIDENCE + assessment.confidence_score = BASE_CONFIDENCE +
(SIMILARITY_WEIGHT * similarity_to_ours) + (SIMILARITY_WEIGHT * similarity_to_ours) +
(CHANGE_RATIO_WEIGHT * change_ratio); (CHANGE_RATIO_WEIGHT * change_ratio);
// Add recommendations // Add recommendations
if (assessment.level >= RiskLevel::MEDIUM) { if (assessment.level >= RiskLevel::MEDIUM) {
assessment.recommendations.push_back("Review changes carefully before accepting"); assessment.recommendations.push_back(
"Review changes carefully before accepting");
} }
if (assessment.has_api_changes) { if (assessment.has_api_changes) {
assessment.recommendations.push_back("Verify API compatibility with dependent code"); assessment.recommendations.push_back(
"Verify API compatibility with dependent code");
} }
if (assessment.affects_critical_section) { if (assessment.affects_critical_section) {
assessment.recommendations.push_back("Test thoroughly, especially security and data operations"); assessment.recommendations.push_back(
"Test thoroughly, especially security and data operations");
} }
if (assessment.risk_factors.empty()) { if (assessment.risk_factors.empty()) {
assessment.recommendations.push_back("Changes appear safe to accept"); assessment.recommendations.push_back("Changes appear safe to accept");
@@ -395,11 +418,9 @@ RiskAssessment analyze_risk_theirs(
return assessment; return assessment;
} }
RiskAssessment analyze_risk_both( RiskAssessment analyze_risk_both(const std::vector<std::string> &base,
const std::vector<std::string>& base,
const std::vector<std::string> &ours, const std::vector<std::string> &ours,
const std::vector<std::string>& theirs const std::vector<std::string> &theirs) {
) {
RiskAssessment assessment; RiskAssessment assessment;
assessment.level = RiskLevel::MEDIUM; // Default to medium for concatenation assessment.level = RiskLevel::MEDIUM; // Default to medium for concatenation
assessment.confidence_score = 0.3; // Lower confidence for concatenation assessment.confidence_score = 0.3; // Lower confidence for concatenation
@@ -410,40 +431,50 @@ RiskAssessment analyze_risk_both(
assessment.affects_critical_section = false; assessment.affects_critical_section = false;
// Concatenating both versions is generally risky // Concatenating both versions is generally risky
assessment.risk_factors.push_back("Concatenating both versions may cause duplicates or conflicts"); assessment.risk_factors.push_back(
"Concatenating both versions may cause duplicates or conflicts");
// Check if either contains critical patterns // Check if either contains critical patterns
if (contains_critical_patterns(ours) || contains_critical_patterns(theirs)) { if (contains_critical_patterns(ours) || contains_critical_patterns(theirs)) {
assessment.affects_critical_section = true; assessment.affects_critical_section = true;
assessment.risk_factors.push_back("Contains critical code patterns that may conflict"); assessment.risk_factors.push_back(
"Contains critical code patterns that may conflict");
assessment.level = RiskLevel::HIGH; assessment.level = RiskLevel::HIGH;
} }
// Check for duplicate logic // Check for duplicate logic
double similarity = calculate_similarity(ours, theirs); double similarity = calculate_similarity(ours, theirs);
if (similarity > 0.5) { if (similarity > 0.5) {
assessment.risk_factors.push_back("High similarity may result in duplicate code"); assessment.risk_factors.push_back(
"High similarity may result in duplicate code");
assessment.level = RiskLevel::HIGH; assessment.level = RiskLevel::HIGH;
} }
// API changes from either side // API changes from either side
if (has_api_signature_changes(base, ours) || has_api_signature_changes(base, theirs)) { if (has_api_signature_changes(base, ours) ||
has_api_signature_changes(base, theirs)) {
assessment.has_api_changes = true; assessment.has_api_changes = true;
assessment.risk_factors.push_back("Multiple API changes may cause conflicts"); assessment.risk_factors.push_back(
"Multiple API changes may cause conflicts");
assessment.level = RiskLevel::HIGH; assessment.level = RiskLevel::HIGH;
} }
// TypeScript interface/type changes from either side // TypeScript interface/type changes from either side
if (has_typescript_interface_changes(base, ours) || has_typescript_interface_changes(base, theirs)) { if (has_typescript_interface_changes(base, ours) ||
has_typescript_interface_changes(base, theirs)) {
assessment.has_api_changes = true; assessment.has_api_changes = true;
assessment.risk_factors.push_back("Multiple TypeScript interface/type changes may cause conflicts"); assessment.risk_factors.push_back(
"Multiple TypeScript interface/type changes may cause conflicts");
assessment.level = RiskLevel::HIGH; assessment.level = RiskLevel::HIGH;
} }
// Recommendations for concatenation // Recommendations for concatenation
assessment.recommendations.push_back("Manual review required - automatic concatenation is risky"); assessment.recommendations.push_back(
assessment.recommendations.push_back("Consider merging logic manually instead of concatenating"); "Manual review required - automatic concatenation is risky");
assessment.recommendations.push_back("Test thoroughly for duplicate or conflicting code"); assessment.recommendations.push_back(
"Consider merging logic manually instead of concatenating");
assessment.recommendations.push_back(
"Test thoroughly for duplicate or conflicting code");
return assessment; return assessment;
} }

View File

@@ -4,11 +4,11 @@
*/ */
#include "wizardmerge/git/git_cli.h" #include "wizardmerge/git/git_cli.h"
#include <cstdlib>
#include <array> #include <array>
#include <sstream> #include <cstdlib>
#include <iostream>
#include <filesystem> #include <filesystem>
#include <iostream>
#include <sstream>
#include <sys/wait.h> #include <sys/wait.h>
namespace wizardmerge { namespace wizardmerge {
@@ -68,12 +68,9 @@ bool is_git_available() {
return result.success; return result.success;
} }
GitResult clone_repository( GitResult clone_repository(const std::string &url,
const std::string& url,
const std::string &destination, const std::string &destination,
const std::string& branch, const std::string &branch, int depth) {
int depth
) {
std::ostringstream cmd; std::ostringstream cmd;
cmd << "git clone"; cmd << "git clone";
@@ -90,11 +87,9 @@ GitResult clone_repository(
return execute_command(cmd.str()); return execute_command(cmd.str());
} }
GitResult create_branch( GitResult create_branch(const std::string &repo_path,
const std::string& repo_path,
const std::string &branch_name, const std::string &branch_name,
const std::string& base_branch const std::string &base_branch) {
) {
std::ostringstream cmd; std::ostringstream cmd;
cmd << "checkout -b \"" << branch_name << "\""; cmd << "checkout -b \"" << branch_name << "\"";
@@ -105,18 +100,14 @@ GitResult create_branch(
return execute_command(git_command(repo_path, cmd.str())); return execute_command(git_command(repo_path, cmd.str()));
} }
GitResult checkout_branch( GitResult checkout_branch(const std::string &repo_path,
const std::string& repo_path, const std::string &branch_name) {
const std::string& branch_name
) {
std::string cmd = "checkout \"" + branch_name + "\""; std::string cmd = "checkout \"" + branch_name + "\"";
return execute_command(git_command(repo_path, cmd)); return execute_command(git_command(repo_path, cmd));
} }
GitResult add_files( GitResult add_files(const std::string &repo_path,
const std::string& repo_path, const std::vector<std::string> &files) {
const std::vector<std::string>& files
) {
if (files.empty()) { if (files.empty()) {
GitResult result; GitResult result;
result.success = true; result.success = true;
@@ -135,15 +126,12 @@ GitResult add_files(
return execute_command(git_command(repo_path, cmd.str())); return execute_command(git_command(repo_path, cmd.str()));
} }
GitResult commit( GitResult commit(const std::string &repo_path, const std::string &message,
const std::string& repo_path, const GitConfig &config) {
const std::string& message,
const GitConfig& config
) {
// Set user config if provided // Set user config if provided
if (!config.user_name.empty() && !config.user_email.empty()) { if (!config.user_name.empty() && !config.user_email.empty()) {
auto name_result = execute_command(git_command(repo_path, auto name_result = execute_command(git_command(
"config user.name \"" + config.user_name + "\"")); repo_path, "config user.name \"" + config.user_name + "\""));
if (!name_result.success) { if (!name_result.success) {
GitResult result; GitResult result;
result.success = false; result.success = false;
@@ -152,8 +140,8 @@ GitResult commit(
return result; return result;
} }
auto email_result = execute_command(git_command(repo_path, auto email_result = execute_command(git_command(
"config user.email \"" + config.user_email + "\"")); repo_path, "config user.email \"" + config.user_email + "\""));
if (!email_result.success) { if (!email_result.success) {
GitResult result; GitResult result;
result.success = false; result.success = false;
@@ -175,13 +163,8 @@ GitResult commit(
return execute_command(git_command(repo_path, cmd)); return execute_command(git_command(repo_path, cmd));
} }
GitResult push( GitResult push(const std::string &repo_path, const std::string &remote,
const std::string& repo_path, const std::string &branch, bool force, const GitConfig &config) {
const std::string& remote,
const std::string& branch,
bool force,
const GitConfig& config
) {
std::ostringstream cmd; std::ostringstream cmd;
cmd << "push"; cmd << "push";
@@ -199,14 +182,17 @@ GitResult push(
if (!config.auth_token.empty()) { if (!config.auth_token.empty()) {
// Note: This assumes HTTPS URLs. For production, use git credential helpers // Note: This assumes HTTPS URLs. For production, use git credential helpers
// or SSH keys for better security // or SSH keys for better security
std::cerr << "Note: Auth token provided. Consider using credential helpers for production." << std::endl; std::cerr << "Note: Auth token provided. Consider using credential helpers "
"for production."
<< std::endl;
} }
return execute_command(full_cmd); return execute_command(full_cmd);
} }
std::optional<std::string> get_current_branch(const std::string &repo_path) { std::optional<std::string> get_current_branch(const std::string &repo_path) {
GitResult result = execute_command(git_command(repo_path, "rev-parse --abbrev-ref HEAD")); GitResult result =
execute_command(git_command(repo_path, "rev-parse --abbrev-ref HEAD"));
if (!result.success) { if (!result.success) {
return std::nullopt; return std::nullopt;
@@ -226,7 +212,8 @@ std::optional<std::string> get_current_branch(const std::string& repo_path) {
return branch; return branch;
} }
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) {
std::string cmd = "rev-parse --verify \"" + branch_name + "\""; std::string cmd = "rev-parse --verify \"" + branch_name + "\"";
GitResult result = execute_command(git_command(repo_path, cmd)); GitResult result = execute_command(git_command(repo_path, cmd));
return result.success; return result.success;

View File

@@ -4,155 +4,35 @@
*/ */
#include "wizardmerge/git/git_platform_client.h" #include "wizardmerge/git/git_platform_client.h"
#include <algorithm>
#include <curl/curl.h>
#include <iostream>
#include <json/json.h>
#include <regex> #include <regex>
#include <sstream> #include <sstream>
#include <iostream>
#include <algorithm>
#include <optional>
#include <vector>
#include <curl/curl.h>
namespace wizardmerge { namespace wizardmerge {
namespace git { namespace git {
namespace { namespace {
std::optional<std::string> extract_object_segment(const std::string& json,
const std::string& key) {
auto key_pos = json.find("\"" + key + "\"");
if (key_pos == std::string::npos) {
return std::nullopt;
}
auto brace_pos = json.find('{', key_pos);
if (brace_pos == std::string::npos) {
return std::nullopt;
}
int depth = 0;
for (size_t i = brace_pos; i < json.size(); ++i) {
if (json[i] == '{') {
depth++;
} else if (json[i] == '}') {
depth--;
if (depth == 0) {
return json.substr(brace_pos, i - brace_pos + 1);
}
}
}
return std::nullopt;
}
std::optional<std::string> extract_array_segment(const std::string& json,
const std::string& key) {
auto key_pos = json.find("\"" + key + "\"");
if (key_pos == std::string::npos) {
return std::nullopt;
}
auto bracket_pos = json.find('[', key_pos);
if (bracket_pos == std::string::npos) {
return std::nullopt;
}
int depth = 0;
for (size_t i = bracket_pos; i < json.size(); ++i) {
if (json[i] == '[') {
depth++;
} else if (json[i] == ']') {
depth--;
if (depth == 0) {
return json.substr(bracket_pos, i - bracket_pos + 1);
}
}
}
return std::nullopt;
}
std::vector<std::string> extract_objects_from_array(const std::string& array) {
std::vector<std::string> objects;
int depth = 0;
std::optional<size_t> start;
for (size_t i = 0; i < array.size(); ++i) {
if (array[i] == '{') {
if (!start) {
start = i;
}
depth++;
} else if (array[i] == '}') {
depth--;
if (depth == 0 && start) {
objects.push_back(array.substr(*start, i - *start + 1));
start.reset();
}
}
}
return objects;
}
std::string extract_string_field(const std::string& json,
const std::string& key,
const std::string& default_value = "") {
std::regex pattern("\\\\\"" + key + "\\\\\"\\s*:\\s*\\\\\"([^\\\\\"]*)\\\\\"");
std::smatch match;
if (std::regex_search(json, match, pattern) && match.size() >= 2) {
return match[1];
}
return default_value;
}
bool extract_bool_field(const std::string& json,
const std::string& key,
bool default_value = false) {
std::regex pattern("\\\\\"" + key + "\\\\\"\\s*:\\s*(true|false)");
std::smatch match;
if (std::regex_search(json, match, pattern) && match.size() >= 2) {
return match[1] == "true";
}
return default_value;
}
int extract_int_field(const std::string& json,
const std::string& key,
int default_value = 0) {
std::regex pattern("\\\\\"" + key + "\\\\\"\\s*:\\s*(-?\\d+)");
std::smatch match;
if (std::regex_search(json, match, pattern) && match.size() >= 2) {
try {
return std::stoi(match[1]);
} catch (const std::exception&) {
}
}
return default_value;
}
/** /**
* @brief Simple base64 decoder * @brief Simple base64 decoder
*/ */
std::string base64_decode(const std::string &encoded) { std::string base64_decode(const std::string &encoded) {
static const std::string base64_chars = static const std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz" "abcdefghijklmnopqrstuvwxyz"
"0123456789+/"; "0123456789+/";
std::string decoded; std::string decoded;
std::vector<int> T(256, -1); std::vector<int> T(256, -1);
for (int i = 0; i < 64; i++) T[base64_chars[i]] = i; for (int i = 0; i < 64; i++)
T[base64_chars[i]] = i;
int val = 0, valb = -8; int val = 0, valb = -8;
for (unsigned char c : encoded) { for (unsigned char c : encoded) {
if (T[c] == -1) break; if (T[c] == -1)
break;
val = (val << 6) + T[c]; val = (val << 6) + T[c];
valb += 6; valb += 6;
if (valb >= 0) { if (valb >= 0) {
@@ -172,7 +52,9 @@ size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
/** /**
* @brief Perform HTTP GET request using libcurl * @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) { bool http_get(const std::string &url, const std::string &token,
std::string &response,
GitPlatform platform = GitPlatform::GitHub) {
CURL *curl = curl_easy_init(); CURL *curl = curl_easy_init();
if (!curl) { if (!curl) {
std::cerr << "Failed to initialize CURL" << std::endl; std::cerr << "Failed to initialize CURL" << std::endl;
@@ -192,7 +74,8 @@ bool http_get(const std::string& url, const std::string& token, std::string& res
struct curl_slist *headers = nullptr; struct curl_slist *headers = nullptr;
if (platform == GitPlatform::GitHub) { if (platform == GitPlatform::GitHub) {
headers = curl_slist_append(headers, "Accept: application/vnd.github.v3+json"); headers =
curl_slist_append(headers, "Accept: application/vnd.github.v3+json");
if (!token.empty()) { if (!token.empty()) {
std::string auth_header = "Authorization: token " + token; std::string auth_header = "Authorization: token " + token;
headers = curl_slist_append(headers, auth_header.c_str()); headers = curl_slist_append(headers, auth_header.c_str());
@@ -243,7 +126,8 @@ bool parse_pr_url(const std::string& url, GitPlatform& platform,
// https://github.com/owner/repo/pull/123 // https://github.com/owner/repo/pull/123
// github.com/owner/repo/pull/123 // github.com/owner/repo/pull/123
std::regex github_regex(R"((?:https?://)?(?:www\.)?github\.com/([^/]+)/([^/]+)/pull/(\d+))"); std::regex github_regex(
R"((?:https?://)?(?:www\.)?github\.com/([^/]+)/([^/]+)/pull/(\d+))");
std::smatch matches; std::smatch matches;
if (std::regex_search(url, matches, github_regex)) { if (std::regex_search(url, matches, github_regex)) {
@@ -260,7 +144,8 @@ bool parse_pr_url(const std::string& url, GitPlatform& platform,
// https://gitlab.com/owner/repo/-/merge_requests/456 // https://gitlab.com/owner/repo/-/merge_requests/456
// gitlab.com/group/subgroup/project/-/merge_requests/789 // gitlab.com/group/subgroup/project/-/merge_requests/789
std::regex gitlab_regex(R"((?:https?://)?(?:www\.)?gitlab\.com/([^/-]+(?:/[^/-]+)*?)/-/merge_requests/(\d+))"); std::regex gitlab_regex(
R"((?:https?://)?(?:www\.)?gitlab\.com/([^/-]+(?:/[^/-]+)*?)/-/merge_requests/(\d+))");
if (std::regex_search(url, matches, gitlab_regex)) { if (std::regex_search(url, matches, gitlab_regex)) {
if (matches.size() == 3) { if (matches.size() == 3) {
@@ -291,13 +176,11 @@ bool parse_pr_url(const std::string& url, GitPlatform& platform,
return false; return false;
} }
std::optional<PullRequest> fetch_pull_request( std::optional<PullRequest> fetch_pull_request(GitPlatform platform,
GitPlatform platform,
const std::string &owner, const std::string &owner,
const std::string &repo, const std::string &repo,
int pr_number, int pr_number,
const std::string& token const std::string &token) {
) {
PullRequest pr; PullRequest pr;
pr.platform = platform; pr.platform = platform;
pr.number = pr_number; pr.number = pr_number;
@@ -308,8 +191,10 @@ std::optional<PullRequest> fetch_pull_request(
if (platform == GitPlatform::GitHub) { if (platform == GitPlatform::GitHub) {
// GitHub API URLs // GitHub API URLs
pr_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + std::to_string(pr_number); pr_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" +
files_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + std::to_string(pr_number) + "/files"; 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) { } else if (platform == GitPlatform::GitLab) {
// GitLab API URLs - encode project path // GitLab API URLs - encode project path
std::string project_path = owner; std::string project_path = owner;
@@ -318,13 +203,16 @@ std::optional<PullRequest> fetch_pull_request(
} }
// URL encode the project path // URL encode the project path
CURL *curl = curl_easy_init(); CURL *curl = curl_easy_init();
char* encoded = curl_easy_escape(curl, project_path.c_str(), project_path.length()); char *encoded =
curl_easy_escape(curl, project_path.c_str(), project_path.length());
std::string encoded_project = encoded; std::string encoded_project = encoded;
curl_free(encoded); curl_free(encoded);
curl_easy_cleanup(curl); curl_easy_cleanup(curl);
pr_url = "https://gitlab.com/api/v4/projects/" + encoded_project + "/merge_requests/" + std::to_string(pr_number); pr_url = "https://gitlab.com/api/v4/projects/" + encoded_project +
files_url = "https://gitlab.com/api/v4/projects/" + encoded_project + "/merge_requests/" + std::to_string(pr_number) + "/changes"; "/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 { } else {
std::cerr << "Unknown platform" << std::endl; std::cerr << "Unknown platform" << std::endl;
return std::nullopt; return std::nullopt;
@@ -337,32 +225,43 @@ std::optional<PullRequest> fetch_pull_request(
return std::nullopt; return std::nullopt;
} }
pr.title = extract_string_field(response, "title"); // Parse JSON response
pr.state = extract_string_field(response, "state"); 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 (platform == GitPlatform::GitHub) {
if (auto base_object = extract_object_segment(response, "base")) { if (root.isMember("base") && root["base"].isObject()) {
pr.base_ref = extract_string_field(*base_object, "ref"); pr.base_ref = root["base"].get("ref", "").asString();
pr.base_sha = extract_string_field(*base_object, "sha"); pr.base_sha = root["base"].get("sha", "").asString();
} }
if (auto head_object = extract_object_segment(response, "head")) { if (root.isMember("head") && root["head"].isObject()) {
pr.head_ref = extract_string_field(*head_object, "ref"); pr.head_ref = root["head"].get("ref", "").asString();
pr.head_sha = extract_string_field(*head_object, "sha"); pr.head_sha = root["head"].get("sha", "").asString();
} }
pr.mergeable = extract_bool_field(response, "mergeable", false); pr.mergeable = root.get("mergeable", false).asBool();
pr.mergeable_state = extract_string_field(response, "mergeable_state", "unknown"); pr.mergeable_state = root.get("mergeable_state", "unknown").asString();
} else if (platform == GitPlatform::GitLab) { } else if (platform == GitPlatform::GitLab) {
pr.base_ref = extract_string_field(response, "target_branch"); pr.base_ref = root.get("target_branch", "").asString();
pr.head_ref = extract_string_field(response, "source_branch"); 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();
if (auto diff_refs = extract_object_segment(response, "diff_refs")) { // GitLab uses different merge status
pr.base_sha = extract_string_field(*diff_refs, "base_sha"); std::string merge_status = root.get("merge_status", "").asString();
pr.head_sha = extract_string_field(*diff_refs, "head_sha");
}
std::string merge_status = extract_string_field(response, "merge_status");
pr.mergeable = (merge_status == "can_be_merged"); pr.mergeable = (merge_status == "can_be_merged");
pr.mergeable_state = merge_status; pr.mergeable_state = merge_status;
} }
@@ -375,32 +274,42 @@ std::optional<PullRequest> fetch_pull_request(
return std::nullopt; return std::nullopt;
} }
Json::Value files_root;
std::istringstream files_stream(files_response);
if (!Json::parseFromStream(reader, files_stream, &files_root, &errs)) {
std::cerr << "Failed to parse files JSON: " << errs << std::endl;
return std::nullopt;
}
// Process files based on platform // Process files based on platform
if (platform == GitPlatform::GitHub) { if (platform == GitPlatform::GitHub && files_root.isArray()) {
for (const auto& file_object : extract_objects_from_array(files_response)) { // GitHub format: array of file objects
for (const auto &file : files_root) {
PRFile pr_file; PRFile pr_file;
pr_file.filename = extract_string_field(file_object, "filename"); pr_file.filename = file.get("filename", "").asString();
pr_file.status = extract_string_field(file_object, "status"); pr_file.status = file.get("status", "").asString();
pr_file.additions = extract_int_field(file_object, "additions"); pr_file.additions = file.get("additions", 0).asInt();
pr_file.deletions = extract_int_field(file_object, "deletions"); pr_file.deletions = file.get("deletions", 0).asInt();
pr_file.changes = extract_int_field(file_object, "changes"); pr_file.changes = file.get("changes", 0).asInt();
pr.files.push_back(pr_file); pr.files.push_back(pr_file);
} }
} else if (platform == GitPlatform::GitLab) { } else if (platform == GitPlatform::GitLab &&
if (auto changes_array = extract_array_segment(files_response, "changes")) { files_root.isMember("changes")) {
for (const auto& file_object : extract_objects_from_array(*changes_array)) { // GitLab format: object with "changes" array
const Json::Value &changes = files_root["changes"];
if (changes.isArray()) {
for (const auto &file : changes) {
PRFile pr_file; PRFile pr_file;
std::string new_path = extract_string_field(file_object, "new_path"); pr_file.filename =
if (!new_path.empty()) { file.get("new_path", file.get("old_path", "").asString())
pr_file.filename = new_path; .asString();
} else {
pr_file.filename = extract_string_field(file_object, "old_path");
}
bool new_file = extract_bool_field(file_object, "new_file", false); // Determine status from new_file, deleted_file, renamed_file flags
bool deleted_file = extract_bool_field(file_object, "deleted_file", false); bool new_file = file.get("new_file", false).asBool();
bool renamed_file = extract_bool_field(file_object, "renamed_file", false); bool deleted_file = file.get("deleted_file", false).asBool();
bool renamed_file = file.get("renamed_file", false).asBool();
if (new_file) { if (new_file) {
pr_file.status = "added"; pr_file.status = "added";
@@ -412,6 +321,8 @@ std::optional<PullRequest> fetch_pull_request(
pr_file.status = "modified"; pr_file.status = "modified";
} }
// GitLab doesn't provide addition/deletion counts in the changes
// endpoint
pr_file.additions = 0; pr_file.additions = 0;
pr_file.deletions = 0; pr_file.deletions = 0;
pr_file.changes = 0; pr_file.changes = 0;
@@ -420,30 +331,21 @@ std::optional<PullRequest> fetch_pull_request(
} }
} }
} }
if (platform == GitPlatform::GitHub || platform == GitPlatform::GitLab) {
// If parsing failed and we have no files, signal an error to callers.
if (pr.files.empty() && !files_response.empty()) {
std::cerr << "No files parsed from API response" << std::endl;
}
} }
return pr; return pr;
} }
std::optional<std::vector<std::string>> fetch_file_content( std::optional<std::vector<std::string>>
GitPlatform platform, fetch_file_content(GitPlatform platform, const std::string &owner,
const std::string& owner, const std::string &repo, const std::string &sha,
const std::string& repo, const std::string &path, const std::string &token) {
const std::string& sha,
const std::string& path,
const std::string& token
) {
std::string url; std::string url;
if (platform == GitPlatform::GitHub) { if (platform == GitPlatform::GitHub) {
// GitHub API URL // GitHub API URL
url = "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" + path + "?ref=" + sha; url = "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" +
path + "?ref=" + sha;
} else if (platform == GitPlatform::GitLab) { } else if (platform == GitPlatform::GitLab) {
// GitLab API URL - encode project path and file path // GitLab API URL - encode project path and file path
std::string project_path = owner; std::string project_path = owner;
@@ -452,7 +354,8 @@ std::optional<std::vector<std::string>> fetch_file_content(
} }
CURL *curl = curl_easy_init(); CURL *curl = curl_easy_init();
char* encoded_project = curl_easy_escape(curl, project_path.c_str(), project_path.length()); 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()); char *encoded_path = curl_easy_escape(curl, path.c_str(), path.length());
url = "https://gitlab.com/api/v4/projects/" + std::string(encoded_project) + url = "https://gitlab.com/api/v4/projects/" + std::string(encoded_project) +
@@ -469,29 +372,46 @@ std::optional<std::vector<std::string>> fetch_file_content(
std::string response; std::string response;
if (!http_get(url, token, response, platform)) { if (!http_get(url, token, response, platform)) {
std::cerr << "Failed to fetch file content for " << path << " at " << sha << std::endl; std::cerr << "Failed to fetch file content for " << path << " at " << sha
<< std::endl;
return std::nullopt; return std::nullopt;
} }
// Handle response based on platform // Handle response based on platform
if (platform == GitPlatform::GitHub) { if (platform == GitPlatform::GitHub) {
// GitHub returns JSON with base64-encoded content // GitHub returns JSON with base64-encoded content
std::string encoding = extract_string_field(response, "encoding"); Json::Value root;
std::string encoded_content = extract_string_field(response, "content"); Json::CharReaderBuilder reader;
std::string errs;
std::istringstream s(response);
if (encoding.empty() || encoded_content.empty()) { 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; std::cerr << "Invalid response format for file content" << std::endl;
return std::nullopt; return std::nullopt;
} }
std::string encoding = root["encoding"].asString();
if (encoding != "base64") { if (encoding != "base64") {
std::cerr << "Unsupported encoding: " << encoding << std::endl; std::cerr << "Unsupported encoding: " << encoding << std::endl;
return std::nullopt; return std::nullopt;
} }
// Decode base64 content
std::string encoded_content = root["content"].asString();
// Remove newlines from base64 string // Remove newlines from base64 string
encoded_content.erase(std::remove(encoded_content.begin(), encoded_content.end(), '\n'), encoded_content.end()); encoded_content.erase(
encoded_content.erase(std::remove(encoded_content.begin(), encoded_content.end(), '\r'), encoded_content.end()); 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 // Decode base64
std::string decoded_content = base64_decode(encoded_content); std::string decoded_content = base64_decode(encoded_content);

View File

@@ -3,9 +3,9 @@
* @brief HTTP API server for WizardMerge using Drogon framework * @brief HTTP API server for WizardMerge using Drogon framework
*/ */
#include <iostream>
#include <drogon/drogon.h>
#include "controllers/MergeController.h" #include "controllers/MergeController.h"
#include <drogon/drogon.h>
#include <iostream>
using namespace drogon; using namespace drogon;
@@ -28,8 +28,8 @@ int main(int argc, char* argv[]) {
auto listeners = app().getListeners(); auto listeners = app().getListeners();
if (!listeners.empty()) { if (!listeners.empty()) {
try { try {
std::cout << "Server will listen on port " std::cout << "Server will listen on port " << listeners[0].toPort
<< listeners[0].toPort << "\n"; << "\n";
} catch (...) { } catch (...) {
std::cout << "Server listener configured\n"; std::cout << "Server listener configured\n";
} }

View File

@@ -20,7 +20,8 @@ bool lines_equal_ignore_whitespace(const std::string& a, const std::string& b) {
auto trim = [](const std::string &s) { auto trim = [](const std::string &s) {
size_t start = s.find_first_not_of(" \t\n\r"); size_t start = s.find_first_not_of(" \t\n\r");
size_t end = s.find_last_not_of(" \t\n\r"); size_t end = s.find_last_not_of(" \t\n\r");
if (start == std::string::npos) return std::string(); if (start == std::string::npos)
return std::string();
return s.substr(start, end - start + 1); return s.substr(start, end - start + 1);
}; };
return trim(a) == trim(b); return trim(a) == trim(b);
@@ -28,11 +29,9 @@ bool lines_equal_ignore_whitespace(const std::string& a, const std::string& b) {
} // namespace } // namespace
MergeResult three_way_merge( MergeResult three_way_merge(const std::vector<std::string> &base,
const std::vector<std::string>& base,
const std::vector<std::string> &ours, const std::vector<std::string> &ours,
const std::vector<std::string>& theirs const std::vector<std::string> &theirs) {
) {
MergeResult result; MergeResult result;
// Simple line-by-line comparison for initial implementation // Simple line-by-line comparison for initial implementation
@@ -79,9 +78,12 @@ MergeResult three_way_merge(
std::vector<std::string> ours_vec = {our_line}; std::vector<std::string> ours_vec = {our_line};
std::vector<std::string> theirs_vec = {their_line}; std::vector<std::string> theirs_vec = {their_line};
conflict.risk_ours = analysis::analyze_risk_ours(base_vec, ours_vec, theirs_vec); conflict.risk_ours =
conflict.risk_theirs = analysis::analyze_risk_theirs(base_vec, ours_vec, theirs_vec); analysis::analyze_risk_ours(base_vec, ours_vec, theirs_vec);
conflict.risk_both = analysis::analyze_risk_both(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); result.conflicts.push_back(conflict);
@@ -110,8 +112,7 @@ MergeResult auto_resolve(const MergeResult& result) {
if (conflict.our_lines.size() == conflict.their_lines.size()) { if (conflict.our_lines.size() == conflict.their_lines.size()) {
can_resolve = true; can_resolve = true;
for (size_t i = 0; i < conflict.our_lines.size(); ++i) { for (size_t i = 0; i < conflict.our_lines.size(); ++i) {
if (!lines_equal_ignore_whitespace( if (!lines_equal_ignore_whitespace(conflict.our_lines[i].content,
conflict.our_lines[i].content,
conflict.their_lines[i].content)) { conflict.their_lines[i].content)) {
can_resolve = false; can_resolve = false;
break; break;

View File

@@ -12,8 +12,7 @@ using namespace wizardmerge::analysis;
* Test basic context analysis * Test basic context analysis
*/ */
TEST(ContextAnalyzerTest, BasicContextAnalysis) { TEST(ContextAnalyzerTest, BasicContextAnalysis) {
std::vector<std::string> lines = { std::vector<std::string> lines = {"#include <iostream>",
"#include <iostream>",
"", "",
"class MyClass {", "class MyClass {",
"public:", "public:",
@@ -22,8 +21,7 @@ TEST(ContextAnalyzerTest, BasicContextAnalysis) {
" int y = 100;", " int y = 100;",
" return;", " return;",
" }", " }",
"};" "};"};
};
auto context = analyze_context(lines, 5, 7); auto context = analyze_context(lines, 5, 7);
@@ -36,12 +34,8 @@ TEST(ContextAnalyzerTest, BasicContextAnalysis) {
* Test function name extraction * Test function name extraction
*/ */
TEST(ContextAnalyzerTest, ExtractFunctionName) { TEST(ContextAnalyzerTest, ExtractFunctionName) {
std::vector<std::string> lines = { std::vector<std::string> lines = {"void testFunction() {", " int x = 10;",
"void testFunction() {", " return;", "}"};
" int x = 10;",
" return;",
"}"
};
std::string func_name = extract_function_name(lines, 1); std::string func_name = extract_function_name(lines, 1);
EXPECT_EQ(func_name, "testFunction"); EXPECT_EQ(func_name, "testFunction");
@@ -51,11 +45,8 @@ TEST(ContextAnalyzerTest, ExtractFunctionName) {
* Test Python function name extraction * Test Python function name extraction
*/ */
TEST(ContextAnalyzerTest, ExtractPythonFunctionName) { TEST(ContextAnalyzerTest, ExtractPythonFunctionName) {
std::vector<std::string> lines = { std::vector<std::string> lines = {"def my_python_function():", " x = 10",
"def my_python_function():", " return x"};
" x = 10",
" return x"
};
std::string func_name = extract_function_name(lines, 1); std::string func_name = extract_function_name(lines, 1);
EXPECT_EQ(func_name, "my_python_function"); EXPECT_EQ(func_name, "my_python_function");
@@ -65,11 +56,8 @@ TEST(ContextAnalyzerTest, ExtractPythonFunctionName) {
* Test class name extraction * Test class name extraction
*/ */
TEST(ContextAnalyzerTest, ExtractClassName) { TEST(ContextAnalyzerTest, ExtractClassName) {
std::vector<std::string> lines = { std::vector<std::string> lines = {"class TestClass {", " int member;",
"class TestClass {", "};"};
" int member;",
"};"
};
std::string class_name = extract_class_name(lines, 1); std::string class_name = extract_class_name(lines, 1);
EXPECT_EQ(class_name, "TestClass"); EXPECT_EQ(class_name, "TestClass");
@@ -80,13 +68,8 @@ TEST(ContextAnalyzerTest, ExtractClassName) {
*/ */
TEST(ContextAnalyzerTest, ExtractImports) { TEST(ContextAnalyzerTest, ExtractImports) {
std::vector<std::string> lines = { std::vector<std::string> lines = {
"#include <iostream>", "#include <iostream>", "#include <vector>", "",
"#include <vector>", "int main() {", " return 0;", "}"};
"",
"int main() {",
" return 0;",
"}"
};
auto imports = extract_imports(lines); auto imports = extract_imports(lines);
EXPECT_EQ(imports.size(), 2); EXPECT_EQ(imports.size(), 2);
@@ -98,10 +81,7 @@ TEST(ContextAnalyzerTest, ExtractImports) {
* Test context with no function * Test context with no function
*/ */
TEST(ContextAnalyzerTest, NoFunctionContext) { TEST(ContextAnalyzerTest, NoFunctionContext) {
std::vector<std::string> lines = { std::vector<std::string> lines = {"int x = 10;", "int y = 20;"};
"int x = 10;",
"int y = 20;"
};
std::string func_name = extract_function_name(lines, 0); std::string func_name = extract_function_name(lines, 0);
EXPECT_EQ(func_name, ""); EXPECT_EQ(func_name, "");
@@ -111,13 +91,8 @@ TEST(ContextAnalyzerTest, NoFunctionContext) {
* Test context window boundaries * Test context window boundaries
*/ */
TEST(ContextAnalyzerTest, ContextWindowBoundaries) { TEST(ContextAnalyzerTest, ContextWindowBoundaries) {
std::vector<std::string> lines = { std::vector<std::string> lines = {"line1", "line2", "line3", "line4",
"line1", "line5"};
"line2",
"line3",
"line4",
"line5"
};
// Test with small context window at beginning of file // Test with small context window at beginning of file
auto context = analyze_context(lines, 0, 0, 2); auto context = analyze_context(lines, 0, 0, 2);
@@ -132,12 +107,9 @@ TEST(ContextAnalyzerTest, ContextWindowBoundaries) {
* Test TypeScript function detection * Test TypeScript function detection
*/ */
TEST(ContextAnalyzerTest, TypeScriptFunctionDetection) { TEST(ContextAnalyzerTest, TypeScriptFunctionDetection) {
std::vector<std::string> lines = { std::vector<std::string> lines = {"export async function fetchData() {",
"export async function fetchData() {",
" const data = await api.get();", " const data = await api.get();",
" return data;", " return data;", "}"};
"}"
};
std::string func_name = extract_function_name(lines, 1); std::string func_name = extract_function_name(lines, 1);
EXPECT_EQ(func_name, "fetchData"); EXPECT_EQ(func_name, "fetchData");
@@ -148,10 +120,8 @@ TEST(ContextAnalyzerTest, TypeScriptFunctionDetection) {
*/ */
TEST(ContextAnalyzerTest, TypeScriptArrowFunctionDetection) { TEST(ContextAnalyzerTest, TypeScriptArrowFunctionDetection) {
std::vector<std::string> lines = { std::vector<std::string> lines = {
"const handleClick = (event: MouseEvent) => {", "const handleClick = (event: MouseEvent) => {", " console.log(event);",
" console.log(event);", "};"};
"};"
};
std::string func_name = extract_function_name(lines, 0); std::string func_name = extract_function_name(lines, 0);
EXPECT_EQ(func_name, "handleClick"); EXPECT_EQ(func_name, "handleClick");
@@ -162,11 +132,7 @@ TEST(ContextAnalyzerTest, TypeScriptArrowFunctionDetection) {
*/ */
TEST(ContextAnalyzerTest, TypeScriptInterfaceDetection) { TEST(ContextAnalyzerTest, TypeScriptInterfaceDetection) {
std::vector<std::string> lines = { std::vector<std::string> lines = {
"export interface User {", "export interface User {", " id: number;", " name: string;", "}"};
" id: number;",
" name: string;",
"}"
};
std::string class_name = extract_class_name(lines, 1); std::string class_name = extract_class_name(lines, 1);
EXPECT_EQ(class_name, "User"); EXPECT_EQ(class_name, "User");
@@ -178,8 +144,7 @@ TEST(ContextAnalyzerTest, TypeScriptInterfaceDetection) {
TEST(ContextAnalyzerTest, TypeScriptTypeAliasDetection) { TEST(ContextAnalyzerTest, TypeScriptTypeAliasDetection) {
std::vector<std::string> lines = { std::vector<std::string> lines = {
"export type Status = 'pending' | 'approved' | 'rejected';", "export type Status = 'pending' | 'approved' | 'rejected';",
"const status: Status = 'pending';" "const status: Status = 'pending';"};
};
std::string type_name = extract_class_name(lines, 0); std::string type_name = extract_class_name(lines, 0);
EXPECT_EQ(type_name, "Status"); EXPECT_EQ(type_name, "Status");
@@ -189,13 +154,8 @@ TEST(ContextAnalyzerTest, TypeScriptTypeAliasDetection) {
* Test TypeScript enum detection * Test TypeScript enum detection
*/ */
TEST(ContextAnalyzerTest, TypeScriptEnumDetection) { TEST(ContextAnalyzerTest, TypeScriptEnumDetection) {
std::vector<std::string> lines = { std::vector<std::string> lines = {"enum Color {", " Red,", " Green,",
"enum Color {", " Blue", "}"};
" Red,",
" Green,",
" Blue",
"}"
};
std::string enum_name = extract_class_name(lines, 1); std::string enum_name = extract_class_name(lines, 1);
EXPECT_EQ(enum_name, "Color"); EXPECT_EQ(enum_name, "Color");
@@ -205,15 +165,13 @@ TEST(ContextAnalyzerTest, TypeScriptEnumDetection) {
* Test TypeScript import extraction * Test TypeScript import extraction
*/ */
TEST(ContextAnalyzerTest, TypeScriptImportExtraction) { TEST(ContextAnalyzerTest, TypeScriptImportExtraction) {
std::vector<std::string> lines = { std::vector<std::string> lines = {"import { Component } from 'react';",
"import { Component } from 'react';",
"import type { User } from './types';", "import type { User } from './types';",
"import * as utils from './utils';", "import * as utils from './utils';",
"", "",
"function MyComponent() {", "function MyComponent() {",
" return null;", " return null;",
"}" "}"};
};
auto imports = extract_imports(lines); auto imports = extract_imports(lines);
EXPECT_GE(imports.size(), 3); EXPECT_GE(imports.size(), 3);

View File

@@ -4,9 +4,9 @@
*/ */
#include "wizardmerge/git/git_cli.h" #include "wizardmerge/git/git_cli.h"
#include <gtest/gtest.h>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <gtest/gtest.h>
using namespace wizardmerge::git; using namespace wizardmerge::git;
namespace fs = std::filesystem; namespace fs = std::filesystem;
@@ -18,7 +18,9 @@ protected:
void SetUp() override { void SetUp() override {
// Create temporary test directory using std::filesystem // Create temporary test directory using std::filesystem
std::filesystem::path temp_base = std::filesystem::temp_directory_path(); std::filesystem::path temp_base = std::filesystem::temp_directory_path();
test_dir = (temp_base / ("wizardmerge_git_test_" + std::to_string(time(nullptr)))).string(); test_dir =
(temp_base / ("wizardmerge_git_test_" + std::to_string(time(nullptr))))
.string();
fs::create_directories(test_dir); fs::create_directories(test_dir);
} }
@@ -33,7 +35,8 @@ protected:
void init_repo(const std::string &path) { void init_repo(const std::string &path) {
system(("git init \"" + path + "\" 2>&1 > /dev/null").c_str()); 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.name \"Test User\"").c_str());
system(("git -C \"" + path + "\" config user.email \"test@example.com\"").c_str()); system(("git -C \"" + path + "\" config user.email \"test@example.com\"")
.c_str());
} }
// Helper to create a file // Helper to create a file
@@ -61,8 +64,11 @@ TEST_F(GitCLITest, BranchExists) {
// Create initial commit (required for branch operations) // Create initial commit (required for branch operations)
create_file(repo_path + "/test.txt", "initial content"); create_file(repo_path + "/test.txt", "initial content");
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str()); system(
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str()); ("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) // Default branch should exist (main or master)
auto current_branch = get_current_branch(repo_path); auto current_branch = get_current_branch(repo_path);
@@ -82,8 +88,11 @@ TEST_F(GitCLITest, GetCurrentBranch) {
// Create initial commit // Create initial commit
create_file(repo_path + "/test.txt", "initial content"); create_file(repo_path + "/test.txt", "initial content");
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str()); system(
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str()); ("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); auto branch = get_current_branch(repo_path);
ASSERT_TRUE(branch.has_value()); ASSERT_TRUE(branch.has_value());
@@ -100,8 +109,11 @@ TEST_F(GitCLITest, CreateBranch) {
// Create initial commit // Create initial commit
create_file(repo_path + "/test.txt", "initial content"); create_file(repo_path + "/test.txt", "initial content");
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str()); system(
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str()); ("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 // Create new branch
GitResult result = create_branch(repo_path, "test-branch"); GitResult result = create_branch(repo_path, "test-branch");
@@ -173,15 +185,20 @@ TEST_F(GitCLITest, CheckoutBranch) {
// Create initial commit // Create initial commit
create_file(repo_path + "/test.txt", "initial content"); create_file(repo_path + "/test.txt", "initial content");
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str()); system(
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str()); ("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 and switch to new branch
create_branch(repo_path, "test-branch"); create_branch(repo_path, "test-branch");
// Get original branch // Get original branch
auto original_branch = get_current_branch(repo_path); auto original_branch = get_current_branch(repo_path);
system(("git -C \"" + repo_path + "\" checkout " + original_branch.value() + " 2>&1 > /dev/null").c_str()); system(("git -C \"" + repo_path + "\" checkout " + original_branch.value() +
" 2>&1 > /dev/null")
.c_str());
// Checkout the test branch // Checkout the test branch
GitResult result = checkout_branch(repo_path, "test-branch"); GitResult result = checkout_branch(repo_path, "test-branch");

View File

@@ -17,21 +17,24 @@ TEST(GitPlatformClientTest, ParseGitHubPRUrl_ValidUrls) {
int pr_number; int pr_number;
// Test full HTTPS URL // Test full HTTPS URL
ASSERT_TRUE(parse_pr_url("https://github.com/owner/repo/pull/123", platform, owner, repo, pr_number)); ASSERT_TRUE(parse_pr_url("https://github.com/owner/repo/pull/123", platform,
owner, repo, pr_number));
EXPECT_EQ(platform, GitPlatform::GitHub); EXPECT_EQ(platform, GitPlatform::GitHub);
EXPECT_EQ(owner, "owner"); EXPECT_EQ(owner, "owner");
EXPECT_EQ(repo, "repo"); EXPECT_EQ(repo, "repo");
EXPECT_EQ(pr_number, 123); EXPECT_EQ(pr_number, 123);
// Test without https:// // Test without https://
ASSERT_TRUE(parse_pr_url("github.com/user/project/pull/456", platform, owner, repo, pr_number)); ASSERT_TRUE(parse_pr_url("github.com/user/project/pull/456", platform, owner,
repo, pr_number));
EXPECT_EQ(platform, GitPlatform::GitHub); EXPECT_EQ(platform, GitPlatform::GitHub);
EXPECT_EQ(owner, "user"); EXPECT_EQ(owner, "user");
EXPECT_EQ(repo, "project"); EXPECT_EQ(repo, "project");
EXPECT_EQ(pr_number, 456); EXPECT_EQ(pr_number, 456);
// Test with www // Test with www
ASSERT_TRUE(parse_pr_url("https://www.github.com/testuser/testrepo/pull/789", platform, owner, repo, pr_number)); 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(platform, GitPlatform::GitHub);
EXPECT_EQ(owner, "testuser"); EXPECT_EQ(owner, "testuser");
EXPECT_EQ(repo, "testrepo"); EXPECT_EQ(repo, "testrepo");
@@ -47,21 +50,25 @@ TEST(GitPlatformClientTest, ParseGitLabMRUrl_ValidUrls) {
int pr_number; int pr_number;
// Test full HTTPS URL // Test full HTTPS URL
ASSERT_TRUE(parse_pr_url("https://gitlab.com/owner/repo/-/merge_requests/123", platform, owner, repo, pr_number)); 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(platform, GitPlatform::GitLab);
EXPECT_EQ(owner, "owner"); EXPECT_EQ(owner, "owner");
EXPECT_EQ(repo, "repo"); EXPECT_EQ(repo, "repo");
EXPECT_EQ(pr_number, 123); EXPECT_EQ(pr_number, 123);
// Test with group/subgroup/project // Test with group/subgroup/project
ASSERT_TRUE(parse_pr_url("https://gitlab.com/group/subgroup/project/-/merge_requests/456", platform, owner, repo, pr_number)); 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(platform, GitPlatform::GitLab);
EXPECT_EQ(owner, "group/subgroup"); EXPECT_EQ(owner, "group/subgroup");
EXPECT_EQ(repo, "project"); EXPECT_EQ(repo, "project");
EXPECT_EQ(pr_number, 456); EXPECT_EQ(pr_number, 456);
// Test without https:// // Test without https://
ASSERT_TRUE(parse_pr_url("gitlab.com/mygroup/myproject/-/merge_requests/789", platform, owner, repo, pr_number)); ASSERT_TRUE(parse_pr_url("gitlab.com/mygroup/myproject/-/merge_requests/789",
platform, owner, repo, pr_number));
EXPECT_EQ(platform, GitPlatform::GitLab); EXPECT_EQ(platform, GitPlatform::GitLab);
EXPECT_EQ(owner, "mygroup"); EXPECT_EQ(owner, "mygroup");
EXPECT_EQ(repo, "myproject"); EXPECT_EQ(repo, "myproject");
@@ -77,19 +84,24 @@ TEST(GitPlatformClientTest, ParsePRUrl_InvalidUrls) {
int pr_number; int pr_number;
// Missing PR number // Missing PR number
EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo/pull/", platform, owner, repo, pr_number)); EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo/pull/", platform,
owner, repo, pr_number));
// Invalid format // Invalid format
EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo", platform, owner, repo, pr_number)); EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo", platform, owner,
repo, pr_number));
// Not a GitHub or GitLab URL // Not a GitHub or GitLab URL
EXPECT_FALSE(parse_pr_url("https://bitbucket.org/owner/repo/pull-requests/123", platform, owner, repo, pr_number)); EXPECT_FALSE(
parse_pr_url("https://bitbucket.org/owner/repo/pull-requests/123",
platform, owner, repo, pr_number));
// Empty string // Empty string
EXPECT_FALSE(parse_pr_url("", platform, owner, repo, pr_number)); EXPECT_FALSE(parse_pr_url("", platform, owner, repo, pr_number));
// Wrong path for GitLab // Wrong path for GitLab
EXPECT_FALSE(parse_pr_url("https://gitlab.com/owner/repo/pull/123", platform, owner, repo, pr_number)); EXPECT_FALSE(parse_pr_url("https://gitlab.com/owner/repo/pull/123", platform,
owner, repo, pr_number));
} }
/** /**
@@ -101,14 +113,18 @@ TEST(GitPlatformClientTest, ParsePRUrl_SpecialCharacters) {
int pr_number; int pr_number;
// GitHub: Underscores and hyphens // 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)); 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(platform, GitPlatform::GitHub);
EXPECT_EQ(owner, "my-owner_123"); EXPECT_EQ(owner, "my-owner_123");
EXPECT_EQ(repo, "my-repo_456"); EXPECT_EQ(repo, "my-repo_456");
EXPECT_EQ(pr_number, 999); EXPECT_EQ(pr_number, 999);
// GitLab: Complex group paths // 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)); 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(platform, GitPlatform::GitLab);
EXPECT_EQ(owner, "org-name/team-1"); EXPECT_EQ(owner, "org-name/team-1");
EXPECT_EQ(repo, "my_project"); EXPECT_EQ(repo, "my_project");

View File

@@ -72,7 +72,8 @@ TEST(RiskAnalyzerTest, RiskAnalysisBoth) {
*/ */
TEST(RiskAnalyzerTest, DetectCriticalPatterns) { TEST(RiskAnalyzerTest, DetectCriticalPatterns) {
std::vector<std::string> safe_code = {"int x = 10;", "return x;"}; std::vector<std::string> safe_code = {"int x = 10;", "return x;"};
std::vector<std::string> unsafe_code = {"delete ptr;", "system(\"rm -rf /\");"}; std::vector<std::string> unsafe_code = {"delete ptr;",
"system(\"rm -rf /\");"};
EXPECT_FALSE(contains_critical_patterns(safe_code)); EXPECT_FALSE(contains_critical_patterns(safe_code));
EXPECT_TRUE(contains_critical_patterns(unsafe_code)); EXPECT_TRUE(contains_critical_patterns(unsafe_code));
@@ -143,17 +144,10 @@ TEST(RiskAnalyzerTest, RiskFactorsPopulated) {
* Test TypeScript interface change detection * Test TypeScript interface change detection
*/ */
TEST(RiskAnalyzerTest, TypeScriptInterfaceChangesDetected) { TEST(RiskAnalyzerTest, TypeScriptInterfaceChangesDetected) {
std::vector<std::string> base = { std::vector<std::string> base = {"interface User {", " name: string;",
"interface User {", "}"};
" name: string;", std::vector<std::string> modified = {"interface User {", " name: string;",
"}" " age: number;", "}"};
};
std::vector<std::string> modified = {
"interface User {",
" name: string;",
" age: number;",
"}"
};
EXPECT_TRUE(has_typescript_interface_changes(base, modified)); EXPECT_TRUE(has_typescript_interface_changes(base, modified));
} }
@@ -162,12 +156,9 @@ TEST(RiskAnalyzerTest, TypeScriptInterfaceChangesDetected) {
* Test TypeScript type alias change detection * Test TypeScript type alias change detection
*/ */
TEST(RiskAnalyzerTest, TypeScriptTypeChangesDetected) { TEST(RiskAnalyzerTest, TypeScriptTypeChangesDetected) {
std::vector<std::string> base = { std::vector<std::string> base = {"type Status = 'pending' | 'approved';"};
"type Status = 'pending' | 'approved';"
};
std::vector<std::string> modified = { std::vector<std::string> modified = {
"type Status = 'pending' | 'approved' | 'rejected';" "type Status = 'pending' | 'approved' | 'rejected';"};
};
EXPECT_TRUE(has_typescript_interface_changes(base, modified)); EXPECT_TRUE(has_typescript_interface_changes(base, modified));
} }
@@ -176,19 +167,10 @@ TEST(RiskAnalyzerTest, TypeScriptTypeChangesDetected) {
* Test TypeScript enum change detection * Test TypeScript enum change detection
*/ */
TEST(RiskAnalyzerTest, TypeScriptEnumChangesDetected) { TEST(RiskAnalyzerTest, TypeScriptEnumChangesDetected) {
std::vector<std::string> base = { std::vector<std::string> base = {"enum Color {", " Red,", " Green",
"enum Color {", "}"};
" Red,", std::vector<std::string> modified = {"enum Color {", " Red,", " Green,",
" Green", " Blue", "}"};
"}"
};
std::vector<std::string> modified = {
"enum Color {",
" Red,",
" Green,",
" Blue",
"}"
};
EXPECT_TRUE(has_typescript_interface_changes(base, modified)); EXPECT_TRUE(has_typescript_interface_changes(base, modified));
} }
@@ -211,11 +193,9 @@ TEST(RiskAnalyzerTest, PackageLockFileDetection) {
*/ */
TEST(RiskAnalyzerTest, TypeScriptCriticalPatternsDetected) { TEST(RiskAnalyzerTest, TypeScriptCriticalPatternsDetected) {
std::vector<std::string> code_with_ts_issues = { std::vector<std::string> code_with_ts_issues = {
"const user = data as any;", "const user = data as any;", "// @ts-ignore",
"// @ts-ignore",
"element.innerHTML = userInput;", "element.innerHTML = userInput;",
"localStorage.setItem('password', pwd);" "localStorage.setItem('password', pwd);"};
};
EXPECT_TRUE(contains_critical_patterns(code_with_ts_issues)); EXPECT_TRUE(contains_critical_patterns(code_with_ts_issues));
} }
@@ -226,10 +206,8 @@ TEST(RiskAnalyzerTest, TypeScriptCriticalPatternsDetected) {
TEST(RiskAnalyzerTest, TypeScriptSafeCodeNoFalsePositives) { TEST(RiskAnalyzerTest, TypeScriptSafeCodeNoFalsePositives) {
std::vector<std::string> safe_code = { std::vector<std::string> safe_code = {
"const user: User = { name: 'John', age: 30 };", "const user: User = { name: 'John', age: 30 };",
"function greet(name: string): string {", "function greet(name: string): string {", " return `Hello, ${name}`;",
" return `Hello, ${name}`;", "}"};
"}"
};
EXPECT_FALSE(contains_critical_patterns(safe_code)); EXPECT_FALSE(contains_critical_patterns(safe_code));
} }
@@ -238,17 +216,10 @@ TEST(RiskAnalyzerTest, TypeScriptSafeCodeNoFalsePositives) {
* Test risk analysis includes TypeScript interface changes * Test risk analysis includes TypeScript interface changes
*/ */
TEST(RiskAnalyzerTest, RiskAnalysisIncludesTypeScriptChanges) { TEST(RiskAnalyzerTest, RiskAnalysisIncludesTypeScriptChanges) {
std::vector<std::string> base = { std::vector<std::string> base = {"interface User {", " name: string;",
"interface User {", "}"};
" name: string;", std::vector<std::string> ours = {"interface User {", " name: string;",
"}" " email: string;", "}"};
};
std::vector<std::string> ours = {
"interface User {",
" name: string;",
" email: string;",
"}"
};
std::vector<std::string> theirs = base; std::vector<std::string> theirs = base;
auto risk = analyze_risk_ours(base, ours, theirs); auto risk = analyze_risk_ours(base, ours, theirs);

View File

@@ -15,7 +15,8 @@ public:
* @param lines Output vector of lines * @param lines Output vector of lines
* @return true if successful, false on error * @return true if successful, false on error
*/ */
static bool readLines(const std::string& filePath, std::vector<std::string>& lines); static bool readLines(const std::string &filePath,
std::vector<std::string> &lines);
/** /**
* @brief Write lines to a file * @brief Write lines to a file
@@ -23,7 +24,8 @@ public:
* @param lines Vector of lines to write * @param lines Vector of lines to write
* @return true if successful, false on error * @return true if successful, false on error
*/ */
static bool writeLines(const std::string& filePath, const std::vector<std::string>& lines); static bool writeLines(const std::string &filePath,
const std::vector<std::string> &lines);
/** /**
* @brief Check if a file exists * @brief Check if a file exists

View File

@@ -1,9 +1,9 @@
#ifndef HTTP_CLIENT_H #ifndef HTTP_CLIENT_H
#define HTTP_CLIENT_H #define HTTP_CLIENT_H
#include <map>
#include <string> #include <string>
#include <vector> #include <vector>
#include <map>
/** /**
* @brief HTTP client for communicating with WizardMerge backend * @brief HTTP client for communicating with WizardMerge backend
@@ -25,13 +25,10 @@ public:
* @param hasConflicts Output whether conflicts were detected * @param hasConflicts Output whether conflicts were detected
* @return true if successful, false on error * @return true if successful, false on error
*/ */
bool performMerge( bool performMerge(const std::vector<std::string> &base,
const std::vector<std::string>& base,
const std::vector<std::string> &ours, const std::vector<std::string> &ours,
const std::vector<std::string> &theirs, const std::vector<std::string> &theirs,
std::vector<std::string>& merged, std::vector<std::string> &merged, bool &hasConflicts);
bool& hasConflicts
);
/** /**
* @brief Check if backend is reachable * @brief Check if backend is reachable
@@ -56,7 +53,8 @@ private:
* @param response Output response string * @param response Output response string
* @return true if successful, false on error * @return true if successful, false on error
*/ */
bool post(const std::string& endpoint, const std::string& jsonBody, std::string& response); bool post(const std::string &endpoint, const std::string &jsonBody,
std::string &response);
}; };
#endif // HTTP_CLIENT_H #endif // HTTP_CLIENT_H

View File

@@ -3,7 +3,8 @@
#include <sstream> #include <sstream>
#include <sys/stat.h> #include <sys/stat.h>
bool FileUtils::readLines(const std::string& filePath, std::vector<std::string>& lines) { bool FileUtils::readLines(const std::string &filePath,
std::vector<std::string> &lines) {
std::ifstream file(filePath); std::ifstream file(filePath);
if (!file.is_open()) { if (!file.is_open()) {
return false; return false;
@@ -19,7 +20,8 @@ bool FileUtils::readLines(const std::string& filePath, std::vector<std::string>&
return true; return true;
} }
bool FileUtils::writeLines(const std::string& filePath, const std::vector<std::string>& lines) { bool FileUtils::writeLines(const std::string &filePath,
const std::vector<std::string> &lines) {
std::ofstream file(filePath); std::ofstream file(filePath);
if (!file.is_open()) { if (!file.is_open()) {
return false; return false;

View File

@@ -1,19 +1,20 @@
#include "http_client.h" #include "http_client.h"
#include <curl/curl.h> #include <curl/curl.h>
#include <sstream>
#include <iostream> #include <iostream>
#include <sstream>
// Callback for libcurl to write response data // Callback for libcurl to write response data
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { static size_t WriteCallback(void *contents, size_t size, size_t nmemb,
void *userp) {
((std::string *)userp)->append((char *)contents, size * nmemb); ((std::string *)userp)->append((char *)contents, size * nmemb);
return size * nmemb; return size * nmemb;
} }
HttpClient::HttpClient(const std::string &backendUrl) HttpClient::HttpClient(const std::string &backendUrl)
: backendUrl_(backendUrl), lastError_("") { : backendUrl_(backendUrl), lastError_("") {}
}
bool HttpClient::post(const std::string& endpoint, const std::string& jsonBody, std::string& response) { bool HttpClient::post(const std::string &endpoint, const std::string &jsonBody,
std::string &response) {
CURL *curl = curl_easy_init(); CURL *curl = curl_easy_init();
if (!curl) { if (!curl) {
lastError_ = "Failed to initialize CURL"; lastError_ = "Failed to initialize CURL";
@@ -45,36 +46,39 @@ bool HttpClient::post(const std::string& endpoint, const std::string& jsonBody,
return success; return success;
} }
bool HttpClient::performMerge( bool HttpClient::performMerge(const std::vector<std::string> &base,
const std::vector<std::string>& base,
const std::vector<std::string> &ours, const std::vector<std::string> &ours,
const std::vector<std::string> &theirs, const std::vector<std::string> &theirs,
std::vector<std::string> &merged, std::vector<std::string> &merged,
bool& hasConflicts bool &hasConflicts) {
) {
// Build JSON request // Build JSON request
// NOTE: This is a simplified JSON builder for prototype purposes. // NOTE: This is a simplified JSON builder for prototype purposes.
// LIMITATION: Does not escape special characters in strings (quotes, backslashes, etc.) // LIMITATION: Does not escape special characters in strings (quotes,
// TODO: For production, use a proper JSON library like nlohmann/json or rapidjson // backslashes, etc.)
// This implementation works for simple test cases but will fail with complex content. // 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; std::ostringstream json;
json << "{"; json << "{";
json << "\"base\":["; json << "\"base\":[";
for (size_t i = 0; i < base.size(); ++i) { for (size_t i = 0; i < base.size(); ++i) {
json << "\"" << base[i] << "\""; // WARNING: No escaping! json << "\"" << base[i] << "\""; // WARNING: No escaping!
if (i < base.size() - 1) json << ","; if (i < base.size() - 1)
json << ",";
} }
json << "],"; json << "],";
json << "\"ours\":["; json << "\"ours\":[";
for (size_t i = 0; i < ours.size(); ++i) { for (size_t i = 0; i < ours.size(); ++i) {
json << "\"" << ours[i] << "\""; // WARNING: No escaping! json << "\"" << ours[i] << "\""; // WARNING: No escaping!
if (i < ours.size() - 1) json << ","; if (i < ours.size() - 1)
json << ",";
} }
json << "],"; json << "],";
json << "\"theirs\":["; json << "\"theirs\":[";
for (size_t i = 0; i < theirs.size(); ++i) { for (size_t i = 0; i < theirs.size(); ++i) {
json << "\"" << theirs[i] << "\""; // WARNING: No escaping! json << "\"" << theirs[i] << "\""; // WARNING: No escaping!
if (i < theirs.size() - 1) json << ","; if (i < theirs.size() - 1)
json << ",";
} }
json << "]"; json << "]";
json << "}"; json << "}";
@@ -87,7 +91,8 @@ bool HttpClient::performMerge(
// Parse JSON response (simple parsing for now) // Parse JSON response (simple parsing for now)
// NOTE: This is a fragile string-based parser for prototype purposes. // NOTE: This is a fragile string-based parser for prototype purposes.
// LIMITATION: Will break on complex JSON or unexpected formatting. // LIMITATION: Will break on complex JSON or unexpected formatting.
// TODO: For production, use a proper JSON library like nlohmann/json or rapidjson // TODO: For production, use a proper JSON library like nlohmann/json or
// rapidjson
merged.clear(); merged.clear();
hasConflicts = (response.find("\"has_conflicts\":true") != std::string::npos); hasConflicts = (response.find("\"has_conflicts\":true") != std::string::npos);
@@ -98,17 +103,21 @@ bool HttpClient::performMerge(
size_t startBracket = response.find("[", mergedPos); size_t startBracket = response.find("[", mergedPos);
size_t endBracket = response.find("]", startBracket); size_t endBracket = response.find("]", startBracket);
if (startBracket != std::string::npos && endBracket != std::string::npos) { if (startBracket != std::string::npos && endBracket != std::string::npos) {
std::string mergedArray = response.substr(startBracket + 1, endBracket - startBracket - 1); std::string mergedArray =
response.substr(startBracket + 1, endBracket - startBracket - 1);
// Parse lines (simplified) // Parse lines (simplified)
size_t pos = 0; size_t pos = 0;
while (pos < mergedArray.size()) { while (pos < mergedArray.size()) {
size_t quoteStart = mergedArray.find("\"", pos); size_t quoteStart = mergedArray.find("\"", pos);
if (quoteStart == std::string::npos) break; if (quoteStart == std::string::npos)
break;
size_t quoteEnd = mergedArray.find("\"", quoteStart + 1); size_t quoteEnd = mergedArray.find("\"", quoteStart + 1);
if (quoteEnd == std::string::npos) break; if (quoteEnd == std::string::npos)
break;
std::string line = mergedArray.substr(quoteStart + 1, quoteEnd - quoteStart - 1); std::string line =
mergedArray.substr(quoteStart + 1, quoteEnd - quoteStart - 1);
merged.push_back(line); merged.push_back(line);
pos = quoteEnd + 1; pos = quoteEnd + 1;
} }
@@ -134,7 +143,8 @@ bool HttpClient::checkBackend() {
bool success = (res == CURLE_OK); bool success = (res == CURLE_OK);
if (!success) { if (!success) {
lastError_ = std::string("Cannot reach backend: ") + curl_easy_strerror(res); lastError_ =
std::string("Cannot reach backend: ") + curl_easy_strerror(res);
} }
curl_easy_cleanup(curl); curl_easy_cleanup(curl);

View File

@@ -1,26 +1,30 @@
#include "http_client.h"
#include "file_utils.h" #include "file_utils.h"
#include <iostream> #include "http_client.h"
#include <cstdlib>
#include <cstring>
#include <curl/curl.h>
#include <fstream> #include <fstream>
#include <iostream>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <cstring>
#include <cstdlib>
#include <curl/curl.h>
/** /**
* @brief Print usage information * @brief Print usage information
*/ */
void printUsage(const char *programName) { void printUsage(const char *programName) {
std::cout << "WizardMerge CLI Frontend - Intelligent Merge Conflict Resolution\n\n"; std::cout
<< "WizardMerge CLI Frontend - Intelligent Merge Conflict Resolution\n\n";
std::cout << "Usage:\n"; std::cout << "Usage:\n";
std::cout << " " << programName << " [OPTIONS] merge --base <file> --ours <file> --theirs <file>\n"; std::cout << " " << programName
std::cout << " " << programName << " [OPTIONS] pr-resolve --url <pr_url> [--token <token>]\n"; << " [OPTIONS] merge --base <file> --ours <file> --theirs <file>\n";
std::cout << " " << programName
<< " [OPTIONS] pr-resolve --url <pr_url> [--token <token>]\n";
std::cout << " " << programName << " [OPTIONS] git-resolve [FILE]\n"; std::cout << " " << programName << " [OPTIONS] git-resolve [FILE]\n";
std::cout << " " << programName << " --help\n"; std::cout << " " << programName << " --help\n";
std::cout << " " << programName << " --version\n\n"; std::cout << " " << programName << " --version\n\n";
std::cout << "Global Options:\n"; std::cout << "Global Options:\n";
std::cout << " --backend <url> Backend server URL (default: http://localhost:8080)\n"; std::cout << " --backend <url> Backend server URL (default: "
"http://localhost:8080)\n";
std::cout << " -v, --verbose Enable verbose output\n"; std::cout << " -v, --verbose Enable verbose output\n";
std::cout << " -q, --quiet Suppress non-error output\n"; std::cout << " -q, --quiet Suppress non-error output\n";
std::cout << " -h, --help Show this help message\n"; std::cout << " -h, --help Show this help message\n";
@@ -31,20 +35,33 @@ void printUsage(const char* programName) {
std::cout << " --ours <file> Our version file (required)\n"; std::cout << " --ours <file> Our version file (required)\n";
std::cout << " --theirs <file> Their version file (required)\n"; std::cout << " --theirs <file> Their version file (required)\n";
std::cout << " -o, --output <file> Output file (default: stdout)\n"; std::cout << " -o, --output <file> Output file (default: stdout)\n";
std::cout << " --format <format> Output format: text, json (default: text)\n\n"; std::cout << " --format <format> Output format: text, json (default: "
"text)\n\n";
std::cout << " pr-resolve Resolve pull request conflicts\n"; std::cout << " pr-resolve Resolve pull request conflicts\n";
std::cout << " --url <url> Pull request URL (required)\n"; std::cout << " --url <url> Pull request URL (required)\n";
std::cout << " --token <token> GitHub API token (optional, can use GITHUB_TOKEN env)\n"; std::cout << " --token <token> GitHub API token (optional, can use "
std::cout << " --branch <name> Create branch with resolved conflicts (optional)\n"; "GITHUB_TOKEN env)\n";
std::cout << " -o, --output <dir> Output directory for resolved files (default: stdout)\n\n"; std::cout << " --branch <name> Create branch with resolved conflicts "
std::cout << " git-resolve Resolve Git merge conflicts (not yet implemented)\n"; "(optional)\n";
std::cout << " -o, --output <dir> Output directory for resolved files "
"(default: stdout)\n\n";
std::cout << " git-resolve Resolve Git merge conflicts (not yet "
"implemented)\n";
std::cout << " [FILE] Specific file to resolve (optional)\n\n"; std::cout << " [FILE] Specific file to resolve (optional)\n\n";
std::cout << "Examples:\n"; std::cout << "Examples:\n";
std::cout << " " << programName << " merge --base base.txt --ours ours.txt --theirs theirs.txt\n"; std::cout << " " << programName
std::cout << " " << programName << " merge --base base.txt --ours ours.txt --theirs theirs.txt -o result.txt\n"; << " merge --base base.txt --ours ours.txt --theirs theirs.txt\n";
std::cout << " " << programName << " pr-resolve --url https://github.com/owner/repo/pull/123\n"; std::cout << " " << programName
std::cout << " " << programName << " pr-resolve --url https://github.com/owner/repo/pull/123 --token ghp_xxx\n"; << " merge --base base.txt --ours ours.txt --theirs theirs.txt -o "
std::cout << " " << programName << " --backend http://remote:8080 merge --base b.txt --ours o.txt --theirs t.txt\n\n"; "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";
} }
/** /**
@@ -52,7 +69,8 @@ void printUsage(const char* programName) {
*/ */
void printVersion() { void printVersion() {
std::cout << "WizardMerge CLI Frontend v1.0.0\n"; std::cout << "WizardMerge CLI Frontend v1.0.0\n";
std::cout << "Part of the WizardMerge Intelligent Merge Conflict Resolution system\n"; std::cout << "Part of the WizardMerge Intelligent Merge Conflict Resolution "
"system\n";
} }
/** /**
@@ -177,7 +195,8 @@ int main(int argc, char* argv[]) {
if (command == "merge") { if (command == "merge") {
// Validate required arguments // Validate required arguments
if (baseFile.empty() || oursFile.empty() || theirsFile.empty()) { if (baseFile.empty() || oursFile.empty() || theirsFile.empty()) {
std::cerr << "Error: merge command requires --base, --ours, and --theirs arguments\n"; std::cerr << "Error: merge command requires --base, --ours, and --theirs "
"arguments\n";
return 2; return 2;
} }
@@ -231,8 +250,10 @@ int main(int argc, char* argv[]) {
} }
if (!client.checkBackend()) { if (!client.checkBackend()) {
std::cerr << "Error: Cannot connect to backend: " << client.getLastError() << "\n"; std::cerr << "Error: Cannot connect to backend: " << client.getLastError()
std::cerr << "Make sure the backend server is running on " << backendUrl << "\n"; << "\n";
std::cerr << "Make sure the backend server is running on " << backendUrl
<< "\n";
return 3; return 3;
} }
@@ -243,14 +264,16 @@ int main(int argc, char* argv[]) {
std::vector<std::string> mergedLines; std::vector<std::string> mergedLines;
bool hasConflicts = false; bool hasConflicts = false;
if (!client.performMerge(baseLines, oursLines, theirsLines, mergedLines, hasConflicts)) { if (!client.performMerge(baseLines, oursLines, theirsLines, mergedLines,
hasConflicts)) {
std::cerr << "Error: Merge failed: " << client.getLastError() << "\n"; std::cerr << "Error: Merge failed: " << client.getLastError() << "\n";
return 1; return 1;
} }
// Output results // Output results
if (!quiet) { if (!quiet) {
std::cout << "Merge completed. Has conflicts: " << (hasConflicts ? "Yes" : "No") << "\n"; std::cout << "Merge completed. Has conflicts: "
<< (hasConflicts ? "Yes" : "No") << "\n";
std::cout << "Result has " << mergedLines.size() << " lines\n"; std::cout << "Result has " << mergedLines.size() << " lines\n";
} }
@@ -283,7 +306,8 @@ int main(int argc, char* argv[]) {
std::cout << "Backend URL: " << backendUrl << "\n"; std::cout << "Backend URL: " << backendUrl << "\n";
std::cout << "Pull Request URL: " << prUrl << "\n"; std::cout << "Pull Request URL: " << prUrl << "\n";
if (!githubToken.empty()) { if (!githubToken.empty()) {
std::cout << "Using GitHub token: " << githubToken.substr(0, 4) << "...\n"; std::cout << "Using GitHub token: " << githubToken.substr(0, 4)
<< "...\n";
} }
} }
@@ -295,8 +319,10 @@ int main(int argc, char* argv[]) {
} }
if (!client.checkBackend()) { if (!client.checkBackend()) {
std::cerr << "Error: Cannot connect to backend: " << client.getLastError() << "\n"; std::cerr << "Error: Cannot connect to backend: " << client.getLastError()
std::cerr << "Make sure the backend server is running on " << backendUrl << "\n"; << "\n";
std::cerr << "Make sure the backend server is running on " << backendUrl
<< "\n";
return 3; return 3;
} }
@@ -329,7 +355,8 @@ int main(int argc, char* argv[]) {
curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.str().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 { auto WriteCallback = [](void *contents, size_t size, size_t nmemb,
void *userp) -> size_t {
((std::string *)userp)->append((char *)contents, size * nmemb); ((std::string *)userp)->append((char *)contents, size * nmemb);
return size * nmemb; return size * nmemb;
}; };
@@ -378,7 +405,8 @@ int main(int argc, char* argv[]) {
return 0; return 0;
} else { } else {
if (!quiet) { if (!quiet) {
std::cerr << "\nFailed to resolve some conflicts. See output for details.\n"; std::cerr
<< "\nFailed to resolve some conflicts. See output for details.\n";
} }
return 1; return 1;
} }

View File

@@ -1,8 +1,8 @@
#include <QCommandLineParser>
#include <QGuiApplication> #include <QGuiApplication>
#include <QNetworkAccessManager>
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext> #include <QQmlContext>
#include <QCommandLineParser>
#include <QNetworkAccessManager>
#include <QUrl> #include <QUrl>
#include <iostream> #include <iostream>
@@ -13,8 +13,7 @@
* supporting both standalone mode (with embedded backend) and client mode * supporting both standalone mode (with embedded backend) and client mode
* (connecting to a remote backend server). * (connecting to a remote backend server).
*/ */
int main(int argc, char *argv[]) int main(int argc, char *argv[]) {
{
QGuiApplication app(argc, argv); QGuiApplication app(argc, argv);
app.setApplicationName("WizardMerge"); app.setApplicationName("WizardMerge");
app.setApplicationVersion("1.0.0"); app.setApplicationVersion("1.0.0");
@@ -23,22 +22,20 @@ int main(int argc, char *argv[])
// Command line parser // Command line parser
QCommandLineParser parser; QCommandLineParser parser;
parser.setApplicationDescription("WizardMerge - Intelligent Merge Conflict Resolution"); parser.setApplicationDescription(
"WizardMerge - Intelligent Merge Conflict Resolution");
parser.addHelpOption(); parser.addHelpOption();
parser.addVersionOption(); parser.addVersionOption();
QCommandLineOption backendUrlOption( QCommandLineOption backendUrlOption(
QStringList() << "b" << "backend-url", QStringList() << "b" << "backend-url",
"Backend server URL (default: http://localhost:8080)", "Backend server URL (default: http://localhost:8080)", "url",
"url", "http://localhost:8080");
"http://localhost:8080"
);
parser.addOption(backendUrlOption); parser.addOption(backendUrlOption);
QCommandLineOption standaloneOption( QCommandLineOption standaloneOption(
QStringList() << "s" << "standalone", QStringList() << "s" << "standalone",
"Run in standalone mode with embedded backend" "Run in standalone mode with embedded backend");
);
parser.addOption(standaloneOption); parser.addOption(standaloneOption);
parser.addPositionalArgument("file", "File to open (optional)"); parser.addPositionalArgument("file", "File to open (optional)");
@@ -49,7 +46,8 @@ int main(int argc, char *argv[])
QString backendUrl = parser.value(backendUrlOption); QString backendUrl = parser.value(backendUrlOption);
bool standalone = parser.isSet(standaloneOption); bool standalone = parser.isSet(standaloneOption);
QStringList positionalArgs = parser.positionalArguments(); QStringList positionalArgs = parser.positionalArguments();
QString filePath = positionalArgs.isEmpty() ? QString() : positionalArgs.first(); QString filePath =
positionalArgs.isEmpty() ? QString() : positionalArgs.first();
// Create QML engine // Create QML engine
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
@@ -63,8 +61,9 @@ int main(int argc, char *argv[])
// Load main QML file // Load main QML file
const QUrl url(u"qrc:/qt/qml/WizardMerge/main.qml"_qs); const QUrl url(u"qrc:/qt/qml/WizardMerge/main.qml"_qs);
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, QObject::connect(
&app, []() { &engine, &QQmlApplicationEngine::objectCreationFailed, &app,
[]() {
std::cerr << "Error: Failed to load QML" << std::endl; std::cerr << "Error: Failed to load QML" << std::endl;
QCoreApplication::exit(-1); QCoreApplication::exit(-1);
}, },

View File

@@ -85,10 +85,10 @@ def parse_spec(jar_path: Path, spec_dir: Path, spec_name: str, output_dir: Path)
# Check result - SANY returns 0 on success and doesn't output "***Parse Error***" # Check result - SANY returns 0 on success and doesn't output "***Parse Error***"
if result.returncode == 0 and "***Parse Error***" not in result.stdout: if result.returncode == 0 and "***Parse Error***" not in result.stdout:
print(f"\n✓ TLA+ specification parsed successfully!") print("\n✓ TLA+ specification parsed successfully!")
return 0 return 0
else: else:
print(f"\n✗ TLA+ specification parsing failed") print("\n✗ TLA+ specification parsing failed")
return 1 return 1
except Exception as e: except Exception as e:
@@ -160,7 +160,7 @@ def run_tlc(jar_path: Path, spec_dir: Path, spec_name: str, output_dir: Path) ->
# Check result # Check result
if result.returncode == 0: if result.returncode == 0:
print(f"\n✓ TLC model checking completed successfully!") print("\n✓ TLC model checking completed successfully!")
return 0 return 0
else: else:
print(f"\n✗ TLC model checking failed with exit code {result.returncode}") print(f"\n✗ TLC model checking failed with exit code {result.returncode}")