mirror of
https://github.com/johndoe6345789/WizardMerge.git
synced 2026-04-25 06:04:55 +00:00
Compare commits
1 Commits
ci/test-re
...
codex/simu
| Author | SHA1 | Date | |
|---|---|---|---|
| b2d25c6f62 |
26
.github/workflows/mirror.yml
vendored
26
.github/workflows/mirror.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -6,165 +6,186 @@
|
|||||||
#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 <string>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
using namespace wizardmerge::analysis;
|
using namespace wizardmerge::analysis;
|
||||||
|
|
||||||
void print_separator() {
|
void print_separator() {
|
||||||
std::cout << "\n" << std::string(60, '=') << "\n" << std::endl;
|
std::cout << "\n" << std::string(60, '=') << "\n" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
std::cout << "WizardMerge TypeScript Support Demo" << std::endl;
|
std::cout << "WizardMerge TypeScript Support Demo" << std::endl;
|
||||||
print_separator();
|
print_separator();
|
||||||
|
|
||||||
// Example 1: TypeScript Function Detection
|
// Example 1: TypeScript Function Detection
|
||||||
std::cout << "Example 1: TypeScript Function Detection" << std::endl;
|
std::cout << "Example 1: TypeScript Function Detection" << std::endl;
|
||||||
std::cout << std::string(40, '-') << std::endl;
|
std::cout << std::string(40, '-') << std::endl;
|
||||||
|
|
||||||
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;
|
||||||
print_separator();
|
print_separator();
|
||||||
|
|
||||||
// Example 2: TypeScript Interface Detection
|
// Example 2: TypeScript Interface Detection
|
||||||
std::cout << "Example 2: TypeScript Interface Detection" << std::endl;
|
std::cout << "Example 2: TypeScript Interface Detection" << std::endl;
|
||||||
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 {", " id: number;", " name: string;",
|
"export interface User {",
|
||||||
" email: string;", "}"};
|
" id: number;",
|
||||||
|
" 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;
|
||||||
print_separator();
|
print_separator();
|
||||||
|
|
||||||
// Example 3: TypeScript Import Extraction
|
// Example 3: TypeScript Import Extraction
|
||||||
std::cout << "Example 3: TypeScript Import Extraction" << std::endl;
|
std::cout << "Example 3: TypeScript Import Extraction" << std::endl;
|
||||||
std::cout << std::string(40, '-') << std::endl;
|
std::cout << std::string(40, '-') << std::endl;
|
||||||
|
|
||||||
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;
|
||||||
for (const auto &import : imports) {
|
for (const auto& import : imports) {
|
||||||
std::cout << " - " << import << std::endl;
|
std::cout << " - " << import << std::endl;
|
||||||
}
|
}
|
||||||
print_separator();
|
print_separator();
|
||||||
|
|
||||||
// Example 4: TypeScript Interface Change Detection
|
// Example 4: TypeScript Interface Change Detection
|
||||||
std::cout << "Example 4: TypeScript Interface Change Detection" << std::endl;
|
std::cout << "Example 4: TypeScript Interface Change Detection" << std::endl;
|
||||||
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 {", " id: number;", " name: string;", "}"};
|
"interface User {",
|
||||||
|
" id: number;",
|
||||||
|
" name: string;",
|
||||||
|
"}"
|
||||||
|
};
|
||||||
|
|
||||||
std::vector<std::string> modified_interface = {
|
std::vector<std::string> modified_interface = {
|
||||||
"interface User {",
|
"interface User {",
|
||||||
" id: number;",
|
" id: number;",
|
||||||
" name: string;",
|
" name: string;",
|
||||||
" email: string; // Added",
|
" email: string; // Added",
|
||||||
" age?: number; // Added optional",
|
" age?: number; // Added optional",
|
||||||
"}"};
|
"}"
|
||||||
|
};
|
||||||
|
|
||||||
bool has_ts_changes =
|
bool has_ts_changes = has_typescript_interface_changes(base_interface, modified_interface);
|
||||||
has_typescript_interface_changes(base_interface, modified_interface);
|
std::cout << "Interface changed: " << (has_ts_changes ? "YES" : "NO") << std::endl;
|
||||||
std::cout << "Interface changed: " << (has_ts_changes ? "YES" : "NO")
|
std::cout << "Risk: Breaking change - affects all usages of User" << std::endl;
|
||||||
<< std::endl;
|
print_separator();
|
||||||
std::cout << "Risk: Breaking change - affects all usages of User"
|
|
||||||
<< std::endl;
|
|
||||||
print_separator();
|
|
||||||
|
|
||||||
// Example 5: TypeScript Critical Pattern Detection
|
// Example 5: TypeScript Critical Pattern Detection
|
||||||
std::cout << "Example 5: TypeScript Critical Pattern Detection" << std::endl;
|
std::cout << "Example 5: TypeScript Critical Pattern Detection" << std::endl;
|
||||||
std::cout << std::string(40, '-') << std::endl;
|
std::cout << std::string(40, '-') << std::endl;
|
||||||
|
|
||||||
std::vector<std::string> risky_code = {
|
std::vector<std::string> risky_code = {
|
||||||
"// Type safety bypass",
|
"// Type safety bypass",
|
||||||
"const user = response.data as any;",
|
"const user = response.data as any;",
|
||||||
"",
|
"",
|
||||||
"// Error suppression",
|
"// Error suppression",
|
||||||
"// @ts-ignore",
|
"// @ts-ignore",
|
||||||
"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::cout << "Contains critical patterns: " << (has_critical ? "YES" : "NO") << std::endl;
|
||||||
<< 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;
|
std::cout << " - Error suppression (@ts-ignore)" << std::endl;
|
||||||
std::cout << " - Error suppression (@ts-ignore)" << std::endl;
|
std::cout << " - XSS vulnerability (innerHTML)" << std::endl;
|
||||||
std::cout << " - XSS vulnerability (innerHTML)" << std::endl;
|
std::cout << " - Insecure password storage (localStorage)" << std::endl;
|
||||||
std::cout << " - Insecure password storage (localStorage)" << std::endl;
|
}
|
||||||
}
|
print_separator();
|
||||||
print_separator();
|
|
||||||
|
|
||||||
// Example 6: Package Lock File Detection
|
// Example 6: Package Lock File Detection
|
||||||
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 = {"package-lock.json", "yarn.lock",
|
std::vector<std::string> lock_files = {
|
||||||
"pnpm-lock.yaml", "bun.lockb",
|
"package-lock.json",
|
||||||
"package.json"};
|
"yarn.lock",
|
||||||
|
"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::cout << file << ": " << (is_lock ? "LOCK FILE" : "regular file") << std::endl;
|
||||||
<< std::endl;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "\nRecommendation for lock file conflicts:" << std::endl;
|
std::cout << "\nRecommendation for lock file conflicts:" << std::endl;
|
||||||
std::cout << " 1. Merge package.json manually" << std::endl;
|
std::cout << " 1. Merge package.json manually" << std::endl;
|
||||||
std::cout << " 2. Delete lock file" << std::endl;
|
std::cout << " 2. Delete lock file" << std::endl;
|
||||||
std::cout << " 3. Run package manager to regenerate" << std::endl;
|
std::cout << " 3. Run package manager to regenerate" << std::endl;
|
||||||
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::cout << "Example 7: Complete Risk Analysis for TypeScript Changes" << std::endl;
|
||||||
<< std::endl;
|
std::cout << std::string(40, '-') << std::endl;
|
||||||
std::cout << std::string(40, '-') << std::endl;
|
|
||||||
|
|
||||||
std::vector<std::string> base = {"interface Config {", " timeout: number;",
|
std::vector<std::string> base = {
|
||||||
"}"};
|
"interface Config {",
|
||||||
|
" timeout: number;",
|
||||||
|
"}"
|
||||||
|
};
|
||||||
|
|
||||||
std::vector<std::string> ours = {"interface Config {", " timeout: number;",
|
std::vector<std::string> ours = {
|
||||||
" retries: number;", "}"};
|
"interface Config {",
|
||||||
|
" timeout: number;",
|
||||||
|
" retries: number;",
|
||||||
|
"}"
|
||||||
|
};
|
||||||
|
|
||||||
std::vector<std::string> theirs = {"interface Config {",
|
std::vector<std::string> theirs = {
|
||||||
" timeout: number;", "}"};
|
"interface Config {",
|
||||||
|
" 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::cout << "Has API Changes: " << (risk.has_api_changes ? "YES" : "NO") << std::endl;
|
||||||
<< 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) {
|
||||||
std::cout << " - " << factor << std::endl;
|
std::cout << " - " << factor << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "\nRecommendations:" << std::endl;
|
std::cout << "\nRecommendations:" << std::endl;
|
||||||
for (const auto &rec : risk.recommendations) {
|
for (const auto& rec : risk.recommendations) {
|
||||||
std::cout << " - " << rec << std::endl;
|
std::cout << " - " << rec << std::endl;
|
||||||
}
|
}
|
||||||
print_separator();
|
print_separator();
|
||||||
|
|
||||||
std::cout << "Demo completed successfully!" << std::endl;
|
std::cout << "Demo completed successfully!" << std::endl;
|
||||||
std::cout << "See docs/TYPESCRIPT_SUPPORT.md for more details." << std::endl;
|
std::cout << "See docs/TYPESCRIPT_SUPPORT.md for more details." << std::endl;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -20,13 +20,13 @@ namespace analysis {
|
|||||||
* @brief Represents code context information for a specific line or region.
|
* @brief Represents code context information for a specific line or region.
|
||||||
*/
|
*/
|
||||||
struct CodeContext {
|
struct CodeContext {
|
||||||
size_t start_line;
|
size_t start_line;
|
||||||
size_t end_line;
|
size_t end_line;
|
||||||
std::vector<std::string> surrounding_lines;
|
std::vector<std::string> surrounding_lines;
|
||||||
std::string function_name;
|
std::string function_name;
|
||||||
std::string class_name;
|
std::string class_name;
|
||||||
std::vector<std::string> imports;
|
std::vector<std::string> imports;
|
||||||
std::map<std::string, std::string> metadata;
|
std::map<std::string, std::string> metadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,9 +42,12 @@ 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(const std::vector<std::string> &lines,
|
CodeContext analyze_context(
|
||||||
size_t start_line, size_t end_line,
|
const std::vector<std::string>& lines,
|
||||||
size_t context_window = 5);
|
size_t start_line,
|
||||||
|
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.
|
||||||
@@ -56,8 +59,10 @@ CodeContext analyze_context(const std::vector<std::string> &lines,
|
|||||||
* @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(const std::vector<std::string> &lines,
|
std::string extract_function_name(
|
||||||
size_t line_number);
|
const std::vector<std::string>& lines,
|
||||||
|
size_t line_number
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Extracts class name from context.
|
* @brief Extracts class name from context.
|
||||||
@@ -69,8 +74,10 @@ std::string extract_function_name(const std::vector<std::string> &lines,
|
|||||||
* @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(const std::vector<std::string> &lines,
|
std::string extract_class_name(
|
||||||
size_t line_number);
|
const std::vector<std::string>& lines,
|
||||||
|
size_t line_number
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Extracts import/include statements from the file.
|
* @brief Extracts import/include statements from the file.
|
||||||
@@ -81,9 +88,11 @@ std::string extract_class_name(const std::vector<std::string> &lines,
|
|||||||
* @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(const std::vector<std::string> &lines);
|
std::vector<std::string> extract_imports(
|
||||||
|
const std::vector<std::string>& lines
|
||||||
|
);
|
||||||
|
|
||||||
} // namespace analysis
|
} // namespace analysis
|
||||||
} // namespace wizardmerge
|
} // namespace wizardmerge
|
||||||
|
|
||||||
#endif // WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H
|
#endif // WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H
|
||||||
|
|||||||
@@ -19,27 +19,27 @@ namespace analysis {
|
|||||||
* @brief Risk level enumeration for merge resolutions.
|
* @brief Risk level enumeration for merge resolutions.
|
||||||
*/
|
*/
|
||||||
enum class RiskLevel {
|
enum class RiskLevel {
|
||||||
LOW, // Safe to merge, minimal risk
|
LOW, // Safe to merge, minimal risk
|
||||||
MEDIUM, // Some risk, review recommended
|
MEDIUM, // Some risk, review recommended
|
||||||
HIGH, // High risk, careful review required
|
HIGH, // High risk, careful review required
|
||||||
CRITICAL // Critical risk, requires expert review
|
CRITICAL // Critical risk, requires expert review
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Detailed risk assessment for a merge resolution.
|
* @brief Detailed risk assessment for a merge resolution.
|
||||||
*/
|
*/
|
||||||
struct RiskAssessment {
|
struct RiskAssessment {
|
||||||
RiskLevel level;
|
RiskLevel level;
|
||||||
double confidence_score; // 0.0 to 1.0
|
double confidence_score; // 0.0 to 1.0
|
||||||
std::vector<std::string> risk_factors;
|
std::vector<std::string> risk_factors;
|
||||||
std::vector<std::string> recommendations;
|
std::vector<std::string> recommendations;
|
||||||
|
|
||||||
// Specific risk indicators
|
// Specific risk indicators
|
||||||
bool has_syntax_changes;
|
bool has_syntax_changes;
|
||||||
bool has_logic_changes;
|
bool has_logic_changes;
|
||||||
bool has_api_changes;
|
bool has_api_changes;
|
||||||
bool affects_multiple_functions;
|
bool affects_multiple_functions;
|
||||||
bool affects_critical_section;
|
bool affects_critical_section;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,9 +50,11 @@ 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(const std::vector<std::string> &base,
|
RiskAssessment analyze_risk_ours(
|
||||||
const std::vector<std::string> &ours,
|
const std::vector<std::string>& base,
|
||||||
const std::vector<std::string> &theirs);
|
const std::vector<std::string>& ours,
|
||||||
|
const std::vector<std::string>& theirs
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Analyzes risk of accepting "theirs" version.
|
* @brief Analyzes risk of accepting "theirs" version.
|
||||||
@@ -62,9 +64,11 @@ RiskAssessment analyze_risk_ours(const std::vector<std::string> &base,
|
|||||||
* @param theirs Their version lines
|
* @param theirs Their version lines
|
||||||
* @return RiskAssessment for accepting theirs
|
* @return RiskAssessment for accepting theirs
|
||||||
*/
|
*/
|
||||||
RiskAssessment analyze_risk_theirs(const std::vector<std::string> &base,
|
RiskAssessment analyze_risk_theirs(
|
||||||
const std::vector<std::string> &ours,
|
const std::vector<std::string>& base,
|
||||||
const std::vector<std::string> &theirs);
|
const std::vector<std::string>& ours,
|
||||||
|
const std::vector<std::string>& theirs
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Analyzes risk of accepting both versions (concatenation).
|
* @brief Analyzes risk of accepting both versions (concatenation).
|
||||||
@@ -74,9 +78,11 @@ RiskAssessment analyze_risk_theirs(const std::vector<std::string> &base,
|
|||||||
* @param theirs Their version lines
|
* @param theirs Their version lines
|
||||||
* @return RiskAssessment for accepting both
|
* @return RiskAssessment for accepting both
|
||||||
*/
|
*/
|
||||||
RiskAssessment analyze_risk_both(const std::vector<std::string> &base,
|
RiskAssessment analyze_risk_both(
|
||||||
const std::vector<std::string> &ours,
|
const std::vector<std::string>& base,
|
||||||
const std::vector<std::string> &theirs);
|
const std::vector<std::string>& ours,
|
||||||
|
const std::vector<std::string>& theirs
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Converts RiskLevel to string representation.
|
* @brief Converts RiskLevel to string representation.
|
||||||
@@ -92,7 +98,7 @@ std::string risk_level_to_string(RiskLevel level);
|
|||||||
* @param lines Lines of code to check
|
* @param lines Lines of code to check
|
||||||
* @return true if critical patterns detected
|
* @return true if critical patterns detected
|
||||||
*/
|
*/
|
||||||
bool contains_critical_patterns(const std::vector<std::string> &lines);
|
bool contains_critical_patterns(const std::vector<std::string>& lines);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Detects if changes affect API signatures.
|
* @brief Detects if changes affect API signatures.
|
||||||
@@ -101,8 +107,10 @@ 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(const std::vector<std::string> &base,
|
bool has_api_signature_changes(
|
||||||
const std::vector<std::string> &modified);
|
const std::vector<std::string>& base,
|
||||||
|
const std::vector<std::string>& modified
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Detects if TypeScript interface or type definitions changed.
|
* @brief Detects if TypeScript interface or type definitions changed.
|
||||||
@@ -111,8 +119,10 @@ bool has_api_signature_changes(const std::vector<std::string> &base,
|
|||||||
* @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(const std::vector<std::string> &base,
|
bool has_typescript_interface_changes(
|
||||||
const std::vector<std::string> &modified);
|
const std::vector<std::string>& base,
|
||||||
|
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.
|
||||||
@@ -120,9 +130,9 @@ bool has_typescript_interface_changes(const std::vector<std::string> &base,
|
|||||||
* @param filename Name of the file
|
* @param filename Name of the file
|
||||||
* @return true if file is package-lock.json
|
* @return true if file is package-lock.json
|
||||||
*/
|
*/
|
||||||
bool is_package_lock_file(const std::string &filename);
|
bool is_package_lock_file(const std::string& filename);
|
||||||
|
|
||||||
} // namespace analysis
|
} // namespace analysis
|
||||||
} // namespace wizardmerge
|
} // namespace wizardmerge
|
||||||
|
|
||||||
#endif // WIZARDMERGE_ANALYSIS_RISK_ANALYZER_H
|
#endif // WIZARDMERGE_ANALYSIS_RISK_ANALYZER_H
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -20,19 +20,19 @@ namespace git {
|
|||||||
* @brief Result of a Git operation
|
* @brief Result of a Git operation
|
||||||
*/
|
*/
|
||||||
struct GitResult {
|
struct GitResult {
|
||||||
bool success;
|
bool success;
|
||||||
std::string output;
|
std::string output;
|
||||||
std::string error;
|
std::string error;
|
||||||
int exit_code;
|
int exit_code;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Configuration for Git operations
|
* @brief Configuration for Git operations
|
||||||
*/
|
*/
|
||||||
struct GitConfig {
|
struct GitConfig {
|
||||||
std::string user_name;
|
std::string user_name;
|
||||||
std::string user_email;
|
std::string user_email;
|
||||||
std::string auth_token; // For HTTPS authentication
|
std::string auth_token; // For HTTPS authentication
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,9 +44,12 @@ 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(const std::string &url,
|
GitResult clone_repository(
|
||||||
const std::string &destination,
|
const std::string& url,
|
||||||
const std::string &branch = "", int depth = 0);
|
const std::string& destination,
|
||||||
|
const std::string& branch = "",
|
||||||
|
int depth = 0
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Create and checkout a new branch
|
* @brief Create and checkout a new branch
|
||||||
@@ -56,9 +59,11 @@ GitResult clone_repository(const std::string &url,
|
|||||||
* @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(const std::string &repo_path,
|
GitResult create_branch(
|
||||||
const std::string &branch_name,
|
const std::string& repo_path,
|
||||||
const std::string &base_branch = "");
|
const std::string& branch_name,
|
||||||
|
const std::string& base_branch = ""
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Checkout an existing branch
|
* @brief Checkout an existing branch
|
||||||
@@ -67,8 +72,10 @@ GitResult create_branch(const std::string &repo_path,
|
|||||||
* @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(const std::string &repo_path,
|
GitResult checkout_branch(
|
||||||
const std::string &branch_name);
|
const std::string& repo_path,
|
||||||
|
const std::string& branch_name
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Stage files for commit
|
* @brief Stage files for commit
|
||||||
@@ -77,8 +84,10 @@ GitResult checkout_branch(const std::string &repo_path,
|
|||||||
* @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(const std::string &repo_path,
|
GitResult add_files(
|
||||||
const std::vector<std::string> &files);
|
const std::string& repo_path,
|
||||||
|
const std::vector<std::string>& files
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Commit staged changes
|
* @brief Commit staged changes
|
||||||
@@ -88,8 +97,11 @@ GitResult add_files(const std::string &repo_path,
|
|||||||
* @param config Optional Git configuration
|
* @param config Optional Git configuration
|
||||||
* @return GitResult with operation status
|
* @return GitResult with operation status
|
||||||
*/
|
*/
|
||||||
GitResult commit(const std::string &repo_path, const std::string &message,
|
GitResult commit(
|
||||||
const GitConfig &config = GitConfig());
|
const std::string& repo_path,
|
||||||
|
const std::string& message,
|
||||||
|
const GitConfig& config = GitConfig()
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Push commits to remote repository
|
* @brief Push commits to remote repository
|
||||||
@@ -101,9 +113,13 @@ GitResult commit(const std::string &repo_path, const std::string &message,
|
|||||||
* @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(const std::string &repo_path, const std::string &remote,
|
GitResult push(
|
||||||
const std::string &branch, bool force = false,
|
const std::string& repo_path,
|
||||||
const GitConfig &config = GitConfig());
|
const std::string& remote,
|
||||||
|
const std::string& branch,
|
||||||
|
bool force = false,
|
||||||
|
const GitConfig& config = GitConfig()
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get current branch name
|
* @brief Get current branch name
|
||||||
@@ -111,7 +127,7 @@ GitResult push(const std::string &repo_path, const std::string &remote,
|
|||||||
* @param repo_path Path to the Git repository
|
* @param repo_path Path to the Git repository
|
||||||
* @return Current branch name, or empty optional on error
|
* @return Current branch name, or empty optional on error
|
||||||
*/
|
*/
|
||||||
std::optional<std::string> get_current_branch(const std::string &repo_path);
|
std::optional<std::string> get_current_branch(const std::string& repo_path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if a branch exists
|
* @brief Check if a branch exists
|
||||||
@@ -120,8 +136,7 @@ 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,
|
bool branch_exists(const std::string& repo_path, const std::string& branch_name);
|
||||||
const std::string &branch_name);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get repository status
|
* @brief Get repository status
|
||||||
@@ -129,7 +144,7 @@ bool branch_exists(const std::string &repo_path,
|
|||||||
* @param repo_path Path to the Git repository
|
* @param repo_path Path to the Git repository
|
||||||
* @return GitResult with status output
|
* @return GitResult with status output
|
||||||
*/
|
*/
|
||||||
GitResult status(const std::string &repo_path);
|
GitResult status(const std::string& repo_path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if Git is available in system PATH
|
* @brief Check if Git is available in system PATH
|
||||||
@@ -138,7 +153,7 @@ GitResult status(const std::string &repo_path);
|
|||||||
*/
|
*/
|
||||||
bool is_git_available();
|
bool is_git_available();
|
||||||
|
|
||||||
} // namespace git
|
} // namespace git
|
||||||
} // namespace wizardmerge
|
} // namespace wizardmerge
|
||||||
|
|
||||||
#endif // WIZARDMERGE_GIT_CLI_H
|
#endif // WIZARDMERGE_GIT_CLI_H
|
||||||
|
|||||||
@@ -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 <map>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace wizardmerge {
|
namespace wizardmerge {
|
||||||
namespace git {
|
namespace git {
|
||||||
@@ -19,36 +19,40 @@ namespace git {
|
|||||||
/**
|
/**
|
||||||
* @brief Supported git platforms
|
* @brief Supported git platforms
|
||||||
*/
|
*/
|
||||||
enum class GitPlatform { GitHub, GitLab, Unknown };
|
enum class GitPlatform {
|
||||||
|
GitHub,
|
||||||
|
GitLab,
|
||||||
|
Unknown
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Information about a file in a pull/merge request
|
* @brief Information about a file in a pull/merge request
|
||||||
*/
|
*/
|
||||||
struct PRFile {
|
struct PRFile {
|
||||||
std::string filename;
|
std::string filename;
|
||||||
std::string status; // "added", "modified", "removed", "renamed"
|
std::string status; // "added", "modified", "removed", "renamed"
|
||||||
int additions;
|
int additions;
|
||||||
int deletions;
|
int deletions;
|
||||||
int changes;
|
int changes;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Pull/merge request information from GitHub or GitLab
|
* @brief Pull/merge request information from GitHub or GitLab
|
||||||
*/
|
*/
|
||||||
struct PullRequest {
|
struct PullRequest {
|
||||||
GitPlatform platform;
|
GitPlatform platform;
|
||||||
int number;
|
int number;
|
||||||
std::string title;
|
std::string title;
|
||||||
std::string state;
|
std::string state;
|
||||||
std::string base_ref; // Base branch name
|
std::string base_ref; // Base branch name
|
||||||
std::string head_ref; // Head branch name
|
std::string head_ref; // Head branch name
|
||||||
std::string base_sha;
|
std::string base_sha;
|
||||||
std::string head_sha;
|
std::string head_sha;
|
||||||
std::string repo_owner;
|
std::string repo_owner;
|
||||||
std::string repo_name;
|
std::string repo_name;
|
||||||
std::vector<PRFile> files;
|
std::vector<PRFile> files;
|
||||||
bool mergeable;
|
bool mergeable;
|
||||||
std::string mergeable_state;
|
std::string mergeable_state;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,8 +71,8 @@ struct PullRequest {
|
|||||||
* @param pr_number Output PR/MR number
|
* @param pr_number Output PR/MR number
|
||||||
* @return true if successfully parsed, false otherwise
|
* @return true if successfully parsed, false otherwise
|
||||||
*/
|
*/
|
||||||
bool parse_pr_url(const std::string &url, GitPlatform &platform,
|
bool parse_pr_url(const std::string& url, GitPlatform& platform,
|
||||||
std::string &owner, std::string &repo, int &pr_number);
|
std::string& owner, std::string& repo, int& pr_number);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Fetch pull/merge request information from GitHub or GitLab API
|
* @brief Fetch pull/merge request information from GitHub or GitLab API
|
||||||
@@ -80,11 +84,13 @@ 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(GitPlatform platform,
|
std::optional<PullRequest> fetch_pull_request(
|
||||||
const std::string &owner,
|
GitPlatform platform,
|
||||||
const std::string &repo,
|
const std::string& owner,
|
||||||
int pr_number,
|
const std::string& repo,
|
||||||
const std::string &token = "");
|
int pr_number,
|
||||||
|
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
|
||||||
@@ -97,12 +103,16 @@ std::optional<PullRequest> fetch_pull_request(GitPlatform platform,
|
|||||||
* @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>>
|
std::optional<std::vector<std::string>> fetch_file_content(
|
||||||
fetch_file_content(GitPlatform platform, const std::string &owner,
|
GitPlatform platform,
|
||||||
const std::string &repo, const std::string &sha,
|
const std::string& owner,
|
||||||
const std::string &path, const std::string &token = "");
|
const std::string& repo,
|
||||||
|
const std::string& sha,
|
||||||
|
const std::string& path,
|
||||||
|
const std::string& token = ""
|
||||||
|
);
|
||||||
|
|
||||||
} // namespace git
|
} // namespace git
|
||||||
} // namespace wizardmerge
|
} // namespace wizardmerge
|
||||||
|
|
||||||
#endif // WIZARDMERGE_GIT_PLATFORM_CLIENT_H
|
#endif // WIZARDMERGE_GIT_PLATFORM_CLIENT_H
|
||||||
|
|||||||
@@ -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 "wizardmerge/analysis/context_analyzer.h"
|
|
||||||
#include "wizardmerge/analysis/risk_analyzer.h"
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include "wizardmerge/analysis/context_analyzer.h"
|
||||||
|
#include "wizardmerge/analysis/risk_analyzer.h"
|
||||||
|
|
||||||
namespace wizardmerge {
|
namespace wizardmerge {
|
||||||
namespace merge {
|
namespace merge {
|
||||||
@@ -22,34 +22,34 @@ namespace merge {
|
|||||||
* @brief Represents a single line in a file with its origin.
|
* @brief Represents a single line in a file with its origin.
|
||||||
*/
|
*/
|
||||||
struct Line {
|
struct Line {
|
||||||
std::string content;
|
std::string content;
|
||||||
enum Origin { BASE, OURS, THEIRS, MERGED } origin;
|
enum Origin { BASE, OURS, THEIRS, MERGED } origin;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Represents a conflict region in the merge result.
|
* @brief Represents a conflict region in the merge result.
|
||||||
*/
|
*/
|
||||||
struct Conflict {
|
struct Conflict {
|
||||||
size_t start_line;
|
size_t start_line;
|
||||||
size_t end_line;
|
size_t end_line;
|
||||||
std::vector<Line> base_lines;
|
std::vector<Line> base_lines;
|
||||||
std::vector<Line> our_lines;
|
std::vector<Line> our_lines;
|
||||||
std::vector<Line> their_lines;
|
std::vector<Line> their_lines;
|
||||||
|
|
||||||
// Context and risk analysis
|
// Context and risk analysis
|
||||||
analysis::CodeContext context;
|
analysis::CodeContext context;
|
||||||
analysis::RiskAssessment risk_ours;
|
analysis::RiskAssessment risk_ours;
|
||||||
analysis::RiskAssessment risk_theirs;
|
analysis::RiskAssessment risk_theirs;
|
||||||
analysis::RiskAssessment risk_both;
|
analysis::RiskAssessment risk_both;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Result of a three-way merge operation.
|
* @brief Result of a three-way merge operation.
|
||||||
*/
|
*/
|
||||||
struct MergeResult {
|
struct MergeResult {
|
||||||
std::vector<Line> merged_lines;
|
std::vector<Line> merged_lines;
|
||||||
std::vector<Conflict> conflicts;
|
std::vector<Conflict> conflicts;
|
||||||
bool has_conflicts() const { return !conflicts.empty(); }
|
bool has_conflicts() const { return !conflicts.empty(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,9 +65,11 @@ 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(const std::vector<std::string> &base,
|
MergeResult three_way_merge(
|
||||||
const std::vector<std::string> &ours,
|
const std::vector<std::string>& base,
|
||||||
const std::vector<std::string> &theirs);
|
const std::vector<std::string>& ours,
|
||||||
|
const std::vector<std::string>& theirs
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Auto-resolves simple non-conflicting patterns.
|
* @brief Auto-resolves simple non-conflicting patterns.
|
||||||
@@ -80,9 +82,9 @@ MergeResult three_way_merge(const std::vector<std::string> &base,
|
|||||||
* @param result The merge result to auto-resolve
|
* @param result The merge result to auto-resolve
|
||||||
* @return Updated merge result with resolved conflicts
|
* @return Updated merge result with resolved conflicts
|
||||||
*/
|
*/
|
||||||
MergeResult auto_resolve(const MergeResult &result);
|
MergeResult auto_resolve(const MergeResult& result);
|
||||||
|
|
||||||
} // namespace merge
|
} // namespace merge
|
||||||
} // namespace wizardmerge
|
} // namespace wizardmerge
|
||||||
|
|
||||||
#endif // WIZARDMERGE_MERGE_THREE_WAY_MERGE_H
|
#endif // WIZARDMERGE_MERGE_THREE_WAY_MERGE_H
|
||||||
|
|||||||
@@ -18,244 +18,241 @@ constexpr size_t IMPORT_SCAN_LIMIT = 50;
|
|||||||
/**
|
/**
|
||||||
* @brief Trim whitespace from string.
|
* @brief Trim whitespace from string.
|
||||||
*/
|
*/
|
||||||
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)
|
if (start == std::string::npos) return "";
|
||||||
return "";
|
return str.substr(start, end - start + 1);
|
||||||
return str.substr(start, end - start + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if a line is a function definition.
|
* @brief Check if a line is a function definition.
|
||||||
*/
|
*/
|
||||||
bool is_function_definition(const std::string &line) {
|
bool is_function_definition(const std::string& line) {
|
||||||
std::string trimmed = trim(line);
|
std::string trimmed = trim(line);
|
||||||
|
|
||||||
// Common function patterns across languages
|
// Common function patterns across languages
|
||||||
std::vector<std::regex> patterns = {
|
std::vector<std::regex> patterns = {
|
||||||
std::regex(
|
std::regex(R"(^\w+\s+\w+\s*\([^)]*\)\s*\{?)"), // C/C++/Java: type name(params)
|
||||||
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
|
std::regex(R"(^\w+\s*:\s*function\s*\([^)]*\))"), // JS object method
|
||||||
// name(params)
|
std::regex(R"(^(public|private|protected)?\s*\w+\s+\w+\s*\([^)]*\))"), // Java/C# methods
|
||||||
std::regex(R"(^\w+\s*:\s*function\s*\([^)]*\))"), // JS object method
|
// TypeScript patterns
|
||||||
std::regex(
|
std::regex(R"(^(export\s+)?(async\s+)?function\s+\w+)"), // TS: export/async function
|
||||||
R"(^(public|private|protected)?\s*\w+\s+\w+\s*\([^)]*\))"), // Java/C#
|
std::regex(R"(^(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>)"), // TS: arrow functions
|
||||||
// methods
|
std::regex(R"(^(public|private|protected|readonly)?\s*\w+\s*\([^)]*\)\s*:\s*\w+)") // TS: typed methods
|
||||||
// TypeScript patterns
|
};
|
||||||
std::regex(
|
|
||||||
R"(^(export\s+)?(async\s+)?function\s+\w+)"), // TS: export/async
|
|
||||||
// function
|
|
||||||
std::regex(
|
|
||||||
R"(^(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>)"), // TS: arrow functions
|
|
||||||
std::regex(
|
|
||||||
R"(^(public|private|protected|readonly)?\s*\w+\s*\([^)]*\)\s*:\s*\w+)") // TS: typed methods
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto &pattern : patterns) {
|
for (const auto& pattern : patterns) {
|
||||||
if (std::regex_search(trimmed, pattern)) {
|
if (std::regex_search(trimmed, pattern)) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Extract function name from a function definition line.
|
* @brief Extract function name from a function definition line.
|
||||||
*/
|
*/
|
||||||
std::string get_function_name_from_line(const std::string &line) {
|
std::string get_function_name_from_line(const std::string& line) {
|
||||||
std::string trimmed = trim(line);
|
std::string trimmed = trim(line);
|
||||||
|
|
||||||
// Try to extract function name using regex
|
// Try to extract function name using regex
|
||||||
std::smatch match;
|
std::smatch match;
|
||||||
|
|
||||||
// Python: def function_name(
|
// Python: def function_name(
|
||||||
std::regex py_pattern(R"(def\s+(\w+)\s*\()");
|
std::regex py_pattern(R"(def\s+(\w+)\s*\()");
|
||||||
if (std::regex_search(trimmed, match, py_pattern)) {
|
if (std::regex_search(trimmed, match, py_pattern)) {
|
||||||
return match[1].str();
|
return match[1].str();
|
||||||
}
|
}
|
||||||
|
|
||||||
// JavaScript/TypeScript: function function_name( or export function
|
// JavaScript/TypeScript: function function_name( or export function function_name(
|
||||||
// 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(
|
std::regex arrow_pattern(R"((?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)");
|
||||||
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();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// C/C++/Java: type function_name(
|
// C/C++/Java: type function_name(
|
||||||
std::regex cpp_pattern(R"(\w+\s+(\w+)\s*\()");
|
std::regex cpp_pattern(R"(\w+\s+(\w+)\s*\()");
|
||||||
if (std::regex_search(trimmed, match, cpp_pattern)) {
|
if (std::regex_search(trimmed, match, cpp_pattern)) {
|
||||||
return match[1].str();
|
return match[1].str();
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if a line is a class definition.
|
* @brief Check if a line is a class definition.
|
||||||
*/
|
*/
|
||||||
bool is_class_definition(const std::string &line) {
|
bool is_class_definition(const std::string& line) {
|
||||||
std::string trimmed = trim(line);
|
std::string trimmed = trim(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
|
std::regex(R"(^(public|private)?\s*class\s+\w+)"), // Java/C#: visibility class Name
|
||||||
// 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(
|
std::regex(R"(^(export\s+)?interface\s+\w+)"), // TS: interface Name
|
||||||
R"(^(export\s+)?(abstract\s+)?class\s+\w+)"), // TS: export class Name
|
std::regex(R"(^(export\s+)?type\s+\w+\s*=)"), // TS: type Name =
|
||||||
std::regex(R"(^(export\s+)?interface\s+\w+)"), // TS: interface Name
|
std::regex(R"(^(export\s+)?enum\s+\w+)") // TS: enum Name
|
||||||
std::regex(R"(^(export\s+)?type\s+\w+\s*=)"), // TS: type Name =
|
};
|
||||||
std::regex(R"(^(export\s+)?enum\s+\w+)") // TS: enum Name
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto &pattern : patterns) {
|
for (const auto& pattern : patterns) {
|
||||||
if (std::regex_search(trimmed, pattern)) {
|
if (std::regex_search(trimmed, pattern)) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Extract class name from a class definition line.
|
* @brief Extract class name from a class definition line.
|
||||||
*/
|
*/
|
||||||
std::string get_class_name_from_line(const std::string &line) {
|
std::string get_class_name_from_line(const std::string& line) {
|
||||||
std::string trimmed = trim(line);
|
std::string trimmed = trim(line);
|
||||||
|
|
||||||
std::smatch match;
|
std::smatch match;
|
||||||
|
|
||||||
// Match class, struct, interface, type, or enum
|
// Match class, struct, interface, type, or enum
|
||||||
std::regex pattern(
|
std::regex pattern(R"((?:export\s+)?(?:abstract\s+)?(class|struct|interface|type|enum)\s+(\w+))");
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // anonymous namespace
|
|
||||||
|
|
||||||
CodeContext analyze_context(const std::vector<std::string> &lines,
|
|
||||||
size_t start_line, size_t end_line,
|
|
||||||
size_t context_window) {
|
|
||||||
CodeContext context;
|
|
||||||
context.start_line = start_line;
|
|
||||||
context.end_line = end_line;
|
|
||||||
|
|
||||||
// Extract surrounding lines
|
|
||||||
size_t window_start =
|
|
||||||
(start_line >= context_window) ? (start_line - context_window) : 0;
|
|
||||||
size_t window_end = std::min(end_line + context_window, lines.size());
|
|
||||||
|
|
||||||
for (size_t i = window_start; i < window_end; ++i) {
|
|
||||||
context.surrounding_lines.push_back(lines[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract function name
|
|
||||||
context.function_name = extract_function_name(lines, start_line);
|
|
||||||
|
|
||||||
// Extract class name
|
|
||||||
context.class_name = extract_class_name(lines, start_line);
|
|
||||||
|
|
||||||
// Extract imports
|
|
||||||
context.imports = extract_imports(lines);
|
|
||||||
|
|
||||||
// Add metadata
|
|
||||||
context.metadata["context_window_start"] = std::to_string(window_start);
|
|
||||||
context.metadata["context_window_end"] = std::to_string(window_end);
|
|
||||||
context.metadata["total_lines"] = std::to_string(lines.size());
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string extract_function_name(const std::vector<std::string> &lines,
|
|
||||||
size_t line_number) {
|
|
||||||
if (line_number >= lines.size()) {
|
|
||||||
return "";
|
return "";
|
||||||
}
|
|
||||||
|
|
||||||
// Check the line itself first
|
|
||||||
if (is_function_definition(lines[line_number])) {
|
|
||||||
return get_function_name_from_line(lines[line_number]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search backwards for function definition
|
|
||||||
for (int i = static_cast<int>(line_number) - 1; i >= 0; --i) {
|
|
||||||
if (is_function_definition(lines[i])) {
|
|
||||||
return get_function_name_from_line(lines[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop searching if we hit a class definition or another function
|
|
||||||
std::string trimmed = trim(lines[i]);
|
|
||||||
if (trimmed.find("class ") == 0 || trimmed.find("struct ") == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string extract_class_name(const std::vector<std::string> &lines,
|
} // anonymous namespace
|
||||||
size_t line_number) {
|
|
||||||
if (line_number >= lines.size()) {
|
CodeContext analyze_context(
|
||||||
|
const std::vector<std::string>& lines,
|
||||||
|
size_t start_line,
|
||||||
|
size_t end_line,
|
||||||
|
size_t context_window
|
||||||
|
) {
|
||||||
|
CodeContext context;
|
||||||
|
context.start_line = start_line;
|
||||||
|
context.end_line = end_line;
|
||||||
|
|
||||||
|
// Extract surrounding lines
|
||||||
|
size_t window_start = (start_line >= context_window) ? (start_line - context_window) : 0;
|
||||||
|
size_t window_end = std::min(end_line + context_window, lines.size());
|
||||||
|
|
||||||
|
for (size_t i = window_start; i < window_end; ++i) {
|
||||||
|
context.surrounding_lines.push_back(lines[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract function name
|
||||||
|
context.function_name = extract_function_name(lines, start_line);
|
||||||
|
|
||||||
|
// Extract class name
|
||||||
|
context.class_name = extract_class_name(lines, start_line);
|
||||||
|
|
||||||
|
// Extract imports
|
||||||
|
context.imports = extract_imports(lines);
|
||||||
|
|
||||||
|
// Add metadata
|
||||||
|
context.metadata["context_window_start"] = std::to_string(window_start);
|
||||||
|
context.metadata["context_window_end"] = std::to_string(window_end);
|
||||||
|
context.metadata["total_lines"] = std::to_string(lines.size());
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string extract_function_name(
|
||||||
|
const std::vector<std::string>& lines,
|
||||||
|
size_t line_number
|
||||||
|
) {
|
||||||
|
if (line_number >= lines.size()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the line itself first
|
||||||
|
if (is_function_definition(lines[line_number])) {
|
||||||
|
return get_function_name_from_line(lines[line_number]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search backwards for function definition
|
||||||
|
for (int i = static_cast<int>(line_number) - 1; i >= 0; --i) {
|
||||||
|
if (is_function_definition(lines[i])) {
|
||||||
|
return get_function_name_from_line(lines[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop searching if we hit a class definition or another function
|
||||||
|
std::string trimmed = trim(lines[i]);
|
||||||
|
if (trimmed.find("class ") == 0 || trimmed.find("struct ") == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
|
||||||
|
|
||||||
// Search backwards for class definition
|
|
||||||
int brace_count = 0;
|
|
||||||
for (int i = static_cast<int>(line_number); i >= 0; --i) {
|
|
||||||
std::string line = lines[i];
|
|
||||||
|
|
||||||
// Count braces to track scope
|
|
||||||
brace_count += std::count(line.begin(), line.end(), '}');
|
|
||||||
brace_count -= std::count(line.begin(), line.end(), '{');
|
|
||||||
|
|
||||||
if (is_class_definition(line) && brace_count <= 0) {
|
|
||||||
return get_class_name_from_line(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string>
|
std::string extract_class_name(
|
||||||
extract_imports(const std::vector<std::string> &lines) {
|
const std::vector<std::string>& lines,
|
||||||
std::vector<std::string> imports;
|
size_t line_number
|
||||||
|
) {
|
||||||
// Scan first lines for imports (imports are typically at the top)
|
if (line_number >= lines.size()) {
|
||||||
size_t scan_limit = std::min(lines.size(), IMPORT_SCAN_LIMIT);
|
return "";
|
||||||
|
|
||||||
for (size_t i = 0; i < scan_limit; ++i) {
|
|
||||||
std::string line = trim(lines[i]);
|
|
||||||
|
|
||||||
// Check for various import patterns
|
|
||||||
if (line.find("#include") == 0 || line.find("import ") == 0 ||
|
|
||||||
line.find("import{") == 0 || // Support both "import{" and "import {"
|
|
||||||
line.find("from ") == 0 || line.find("require(") != std::string::npos ||
|
|
||||||
line.find("using ") == 0 ||
|
|
||||||
// TypeScript/ES6 specific patterns
|
|
||||||
line.find("import *") == 0 || line.find("import type") == 0 ||
|
|
||||||
line.find("export {") == 0 || line.find("export *") == 0) {
|
|
||||||
imports.push_back(line);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return imports;
|
// Search backwards for class definition
|
||||||
|
int brace_count = 0;
|
||||||
|
for (int i = static_cast<int>(line_number); i >= 0; --i) {
|
||||||
|
std::string line = lines[i];
|
||||||
|
|
||||||
|
// Count braces to track scope
|
||||||
|
brace_count += std::count(line.begin(), line.end(), '}');
|
||||||
|
brace_count -= std::count(line.begin(), line.end(), '{');
|
||||||
|
|
||||||
|
if (is_class_definition(line) && brace_count <= 0) {
|
||||||
|
return get_class_name_from_line(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace analysis
|
std::vector<std::string> extract_imports(
|
||||||
} // namespace wizardmerge
|
const std::vector<std::string>& lines
|
||||||
|
) {
|
||||||
|
std::vector<std::string> imports;
|
||||||
|
|
||||||
|
// Scan first lines for imports (imports are typically at the top)
|
||||||
|
size_t scan_limit = std::min(lines.size(), IMPORT_SCAN_LIMIT);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < scan_limit; ++i) {
|
||||||
|
std::string line = trim(lines[i]);
|
||||||
|
|
||||||
|
// Check for various import patterns
|
||||||
|
if (line.find("#include") == 0 ||
|
||||||
|
line.find("import ") == 0 ||
|
||||||
|
line.find("import{") == 0 || // Support both "import{" and "import {"
|
||||||
|
line.find("from ") == 0 ||
|
||||||
|
line.find("require(") != std::string::npos ||
|
||||||
|
line.find("using ") == 0 ||
|
||||||
|
// TypeScript/ES6 specific patterns
|
||||||
|
line.find("import *") == 0 ||
|
||||||
|
line.find("import type") == 0 ||
|
||||||
|
line.find("export {") == 0 ||
|
||||||
|
line.find("export *") == 0) {
|
||||||
|
imports.push_back(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return imports;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace analysis
|
||||||
|
} // namespace wizardmerge
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
#include "wizardmerge/analysis/risk_analyzer.h"
|
#include "wizardmerge/analysis/risk_analyzer.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace wizardmerge {
|
namespace wizardmerge {
|
||||||
namespace analysis {
|
namespace analysis {
|
||||||
@@ -14,470 +14,439 @@ namespace analysis {
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Confidence score weights for risk assessment
|
// Confidence score weights for risk assessment
|
||||||
constexpr double BASE_CONFIDENCE = 0.5; // Base confidence level
|
constexpr double BASE_CONFIDENCE = 0.5; // Base confidence level
|
||||||
constexpr double SIMILARITY_WEIGHT = 0.3; // Weight for code similarity
|
constexpr double SIMILARITY_WEIGHT = 0.3; // Weight for code similarity
|
||||||
constexpr double CHANGE_RATIO_WEIGHT = 0.2; // Weight for change ratio
|
constexpr double CHANGE_RATIO_WEIGHT = 0.2; // Weight for change ratio
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Trim whitespace from string.
|
* @brief Trim whitespace from string.
|
||||||
*/
|
*/
|
||||||
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)
|
if (start == std::string::npos) return "";
|
||||||
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(const std::vector<std::string> &lines1,
|
double calculate_similarity(
|
||||||
const std::vector<std::string> &lines2) {
|
const std::vector<std::string>& lines1,
|
||||||
if (lines1.empty() && lines2.empty())
|
const std::vector<std::string>& lines2
|
||||||
return 1.0;
|
) {
|
||||||
if (lines1.empty() || lines2.empty())
|
if (lines1.empty() && lines2.empty()) return 1.0;
|
||||||
return 0.0;
|
if (lines1.empty() || lines2.empty()) return 0.0;
|
||||||
|
|
||||||
// Simple Jaccard similarity on lines
|
// Simple Jaccard similarity on lines
|
||||||
size_t common_lines = 0;
|
size_t common_lines = 0;
|
||||||
for (const auto &line1 : lines1) {
|
for (const auto& line1 : lines1) {
|
||||||
if (std::find(lines2.begin(), lines2.end(), line1) != lines2.end()) {
|
if (std::find(lines2.begin(), lines2.end(), line1) != lines2.end()) {
|
||||||
common_lines++;
|
common_lines++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return total_unique > 0 ? static_cast<double>(common_lines) / total_unique : 0.0;
|
||||||
: 0.0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Count number of changed lines between two versions.
|
* @brief Count number of changed lines between two versions.
|
||||||
*/
|
*/
|
||||||
size_t count_changes(const std::vector<std::string> &base,
|
size_t count_changes(
|
||||||
const std::vector<std::string> &modified) {
|
const std::vector<std::string>& base,
|
||||||
size_t changes = 0;
|
const std::vector<std::string>& modified
|
||||||
size_t max_len = std::max(base.size(), modified.size());
|
) {
|
||||||
|
size_t changes = 0;
|
||||||
|
size_t max_len = std::max(base.size(), modified.size());
|
||||||
|
|
||||||
for (size_t i = 0; i < max_len; ++i) {
|
for (size_t i = 0; i < max_len; ++i) {
|
||||||
std::string base_line = (i < base.size()) ? base[i] : "";
|
std::string base_line = (i < base.size()) ? base[i] : "";
|
||||||
std::string mod_line = (i < modified.size()) ? modified[i] : "";
|
std::string mod_line = (i < modified.size()) ? modified[i] : "";
|
||||||
|
|
||||||
if (base_line != mod_line) {
|
if (base_line != mod_line) {
|
||||||
changes++;
|
changes++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if line contains function or method definition.
|
* @brief Check if line contains function or method definition.
|
||||||
*/
|
*/
|
||||||
bool is_function_signature(const std::string &line) {
|
bool is_function_signature(const std::string& line) {
|
||||||
std::string trimmed = trim(line);
|
std::string trimmed = trim(line);
|
||||||
|
|
||||||
std::vector<std::regex> patterns = {
|
std::vector<std::regex> patterns = {
|
||||||
std::regex(R"(^\w+\s+\w+\s*\([^)]*\))"), // C/C++/Java
|
std::regex(R"(^\w+\s+\w+\s*\([^)]*\))"), // C/C++/Java
|
||||||
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(
|
std::regex(R"(^(export\s+)?(async\s+)?function\s+\w+\s*\([^)]*\))"), // TS function
|
||||||
R"(^(export\s+)?(async\s+)?function\s+\w+\s*\([^)]*\))"), // TS
|
std::regex(R"(^(const|let|var)\s+\w+\s*=\s*\([^)]*\)\s*=>)"), // Arrow function
|
||||||
// function
|
std::regex(R"(^\w+\s*\([^)]*\)\s*:\s*\w+)"), // TS: method with return type
|
||||||
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) {
|
||||||
if (std::regex_search(trimmed, pattern)) {
|
if (std::regex_search(trimmed, pattern)) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
std::string risk_level_to_string(RiskLevel level) {
|
std::string risk_level_to_string(RiskLevel level) {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case RiskLevel::LOW:
|
case RiskLevel::LOW: return "low";
|
||||||
return "low";
|
case RiskLevel::MEDIUM: return "medium";
|
||||||
case RiskLevel::MEDIUM:
|
case RiskLevel::HIGH: return "high";
|
||||||
return "medium";
|
case RiskLevel::CRITICAL: return "critical";
|
||||||
case RiskLevel::HIGH:
|
default: return "unknown";
|
||||||
return "high";
|
}
|
||||||
case RiskLevel::CRITICAL:
|
|
||||||
return "critical";
|
|
||||||
default:
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool contains_critical_patterns(const std::vector<std::string> &lines) {
|
bool contains_critical_patterns(const std::vector<std::string>& lines) {
|
||||||
std::vector<std::regex> critical_patterns = {
|
std::vector<std::regex> critical_patterns = {
|
||||||
std::regex(R"(delete\s+\w+)"), // Delete operations
|
std::regex(R"(delete\s+\w+)"), // Delete operations
|
||||||
std::regex(R"(drop\s+(table|database))"), // Database drops
|
std::regex(R"(drop\s+(table|database))"), // Database drops
|
||||||
std::regex(R"(rm\s+-rf)"), // Destructive file operations
|
std::regex(R"(rm\s+-rf)"), // Destructive file operations
|
||||||
std::regex(R"(eval\s*\()"), // Eval (security risk)
|
std::regex(R"(eval\s*\()"), // Eval (security risk)
|
||||||
std::regex(R"(exec\s*\()"), // Exec (security risk)
|
std::regex(R"(exec\s*\()"), // Exec (security risk)
|
||||||
std::regex(R"(system\s*\()"), // System calls
|
std::regex(R"(system\s*\()"), // System calls
|
||||||
std::regex(R"(\.password\s*=)"), // Password assignments
|
std::regex(R"(\.password\s*=)"), // Password assignments
|
||||||
std::regex(R"(\.secret\s*=)"), // Secret assignments
|
std::regex(R"(\.secret\s*=)"), // Secret assignments
|
||||||
std::regex(R"(sudo\s+)"), // Sudo usage
|
std::regex(R"(sudo\s+)"), // Sudo usage
|
||||||
std::regex(R"(chmod\s+777)"), // Overly permissive permissions
|
std::regex(R"(chmod\s+777)"), // Overly permissive permissions
|
||||||
// TypeScript specific critical patterns
|
// TypeScript specific critical patterns
|
||||||
std::regex(R"(dangerouslySetInnerHTML)"), // React XSS risk
|
std::regex(R"(dangerouslySetInnerHTML)"), // React XSS risk
|
||||||
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
|
std::regex(R"(localStorage\.setItem.*password)"), // Storing passwords in localStorage
|
||||||
// localStorage
|
std::regex(R"(innerHTML\s*=)"), // XSS risk
|
||||||
std::regex(R"(innerHTML\s*=)"), // XSS risk
|
};
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto &line : lines) {
|
for (const auto& line : lines) {
|
||||||
std::string trimmed = trim(line);
|
std::string trimmed = trim(line);
|
||||||
for (const auto &pattern : critical_patterns) {
|
for (const auto& pattern : critical_patterns) {
|
||||||
if (std::regex_search(trimmed, pattern)) {
|
if (std::regex_search(trimmed, pattern)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool has_api_signature_changes(const std::vector<std::string> &base,
|
bool has_api_signature_changes(
|
||||||
const std::vector<std::string> &modified) {
|
const std::vector<std::string>& base,
|
||||||
// Check if function signatures changed
|
const std::vector<std::string>& modified
|
||||||
for (size_t i = 0; i < base.size() && i < modified.size(); ++i) {
|
) {
|
||||||
bool base_is_sig = is_function_signature(base[i]);
|
// Check if function signatures changed
|
||||||
bool mod_is_sig = is_function_signature(modified[i]);
|
for (size_t i = 0; i < base.size() && i < modified.size(); ++i) {
|
||||||
|
bool base_is_sig = is_function_signature(base[i]);
|
||||||
|
bool mod_is_sig = is_function_signature(modified[i]);
|
||||||
|
|
||||||
if (base_is_sig && mod_is_sig && base[i] != modified[i]) {
|
if (base_is_sig && mod_is_sig && base[i] != modified[i]) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
) {
|
||||||
static const std::vector<std::regex> ts_definition_patterns = {
|
// Use static regex patterns to avoid recompilation
|
||||||
std::regex(R"(\binterface\s+\w+)"),
|
static const std::vector<std::regex> ts_definition_patterns = {
|
||||||
std::regex(R"(\btype\s+\w+\s*=)"),
|
std::regex(R"(\binterface\s+\w+)"),
|
||||||
std::regex(R"(\benum\s+\w+)"),
|
std::regex(R"(\btype\s+\w+\s*=)"),
|
||||||
};
|
std::regex(R"(\benum\s+\w+)"),
|
||||||
|
};
|
||||||
|
|
||||||
// Check if any TypeScript definition exists in base
|
// Check if any TypeScript definition exists in base
|
||||||
bool base_has_ts_def = false;
|
bool base_has_ts_def = false;
|
||||||
for (const auto &line : base) {
|
for (const auto& line : base) {
|
||||||
std::string trimmed = trim(line);
|
std::string trimmed = trim(line);
|
||||||
for (const auto &pattern : ts_definition_patterns) {
|
for (const auto& pattern : ts_definition_patterns) {
|
||||||
if (std::regex_search(trimmed, pattern)) {
|
if (std::regex_search(trimmed, pattern)) {
|
||||||
base_has_ts_def = true;
|
base_has_ts_def = true;
|
||||||
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
|
||||||
bool modified_has_ts_def = false;
|
bool modified_has_ts_def = false;
|
||||||
for (const auto &line : modified) {
|
for (const auto& line : modified) {
|
||||||
std::string trimmed = trim(line);
|
std::string trimmed = trim(line);
|
||||||
for (const auto &pattern : ts_definition_patterns) {
|
for (const auto& pattern : ts_definition_patterns) {
|
||||||
if (std::regex_search(trimmed, pattern)) {
|
if (std::regex_search(trimmed, pattern)) {
|
||||||
modified_has_ts_def = true;
|
modified_has_ts_def = true;
|
||||||
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
|
||||||
if (base_has_ts_def || modified_has_ts_def) {
|
if (base_has_ts_def || modified_has_ts_def) {
|
||||||
// Check if the actual content changed
|
// Check if the actual content changed
|
||||||
if (base.size() != modified.size()) {
|
if (base.size() != modified.size()) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
// Cache trimmed lines to avoid repeated trim() calls
|
||||||
|
for (size_t i = 0; i < base.size(); ++i) {
|
||||||
|
std::string base_trimmed = trim(base[i]);
|
||||||
|
std::string mod_trimmed = trim(modified[i]);
|
||||||
|
if (base_trimmed != mod_trimmed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Cache trimmed lines to avoid repeated trim() calls
|
|
||||||
for (size_t i = 0; i < base.size(); ++i) {
|
|
||||||
std::string base_trimmed = trim(base[i]);
|
|
||||||
std::string mod_trimmed = trim(modified[i]);
|
|
||||||
if (base_trimmed != mod_trimmed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_package_lock_file(const std::string &filename) {
|
bool is_package_lock_file(const std::string& filename) {
|
||||||
// Check for package-lock.json, yarn.lock, pnpm-lock.yaml, etc.
|
// Check for package-lock.json, yarn.lock, pnpm-lock.yaml, etc.
|
||||||
return filename.find("package-lock.json") != std::string::npos ||
|
return filename.find("package-lock.json") != std::string::npos ||
|
||||||
filename.find("yarn.lock") != std::string::npos ||
|
filename.find("yarn.lock") != std::string::npos ||
|
||||||
filename.find("pnpm-lock.yaml") != std::string::npos ||
|
filename.find("pnpm-lock.yaml") != std::string::npos ||
|
||||||
filename.find("bun.lockb") != std::string::npos;
|
filename.find("bun.lockb") != std::string::npos;
|
||||||
}
|
}
|
||||||
|
|
||||||
RiskAssessment analyze_risk_ours(const std::vector<std::string> &base,
|
RiskAssessment analyze_risk_ours(
|
||||||
const std::vector<std::string> &ours,
|
const std::vector<std::string>& base,
|
||||||
const std::vector<std::string> &theirs) {
|
const std::vector<std::string>& ours,
|
||||||
RiskAssessment assessment;
|
const std::vector<std::string>& theirs
|
||||||
assessment.level = RiskLevel::LOW;
|
) {
|
||||||
assessment.confidence_score = 0.5;
|
RiskAssessment assessment;
|
||||||
assessment.has_syntax_changes = false;
|
assessment.level = RiskLevel::LOW;
|
||||||
assessment.has_logic_changes = false;
|
assessment.confidence_score = 0.5;
|
||||||
assessment.has_api_changes = false;
|
assessment.has_syntax_changes = false;
|
||||||
assessment.affects_multiple_functions = false;
|
assessment.has_logic_changes = false;
|
||||||
assessment.affects_critical_section = false;
|
assessment.has_api_changes = false;
|
||||||
|
assessment.affects_multiple_functions = false;
|
||||||
|
assessment.affects_critical_section = false;
|
||||||
|
|
||||||
// Calculate changes
|
// Calculate changes
|
||||||
size_t our_changes = count_changes(base, ours);
|
size_t our_changes = count_changes(base, ours);
|
||||||
size_t their_changes = count_changes(base, theirs);
|
size_t their_changes = count_changes(base, theirs);
|
||||||
double similarity_to_theirs = calculate_similarity(ours, theirs);
|
double similarity_to_theirs = calculate_similarity(ours, theirs);
|
||||||
|
|
||||||
// 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(
|
assessment.risk_factors.push_back("Contains critical code patterns (security/data operations)");
|
||||||
"Contains critical code patterns (security/data operations)");
|
assessment.level = RiskLevel::HIGH;
|
||||||
assessment.level = RiskLevel::HIGH;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for API changes
|
|
||||||
if (has_api_signature_changes(base, ours)) {
|
|
||||||
assessment.has_api_changes = true;
|
|
||||||
assessment.risk_factors.push_back("Function/method signatures changed");
|
|
||||||
if (assessment.level < RiskLevel::MEDIUM) {
|
|
||||||
assessment.level = RiskLevel::MEDIUM;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check for TypeScript interface/type changes
|
// Check for API changes
|
||||||
if (has_typescript_interface_changes(base, ours)) {
|
if (has_api_signature_changes(base, ours)) {
|
||||||
assessment.has_api_changes = true;
|
assessment.has_api_changes = true;
|
||||||
assessment.risk_factors.push_back(
|
assessment.risk_factors.push_back("Function/method signatures changed");
|
||||||
"TypeScript interface or type definitions changed");
|
if (assessment.level < RiskLevel::MEDIUM) {
|
||||||
if (assessment.level < RiskLevel::MEDIUM) {
|
assessment.level = RiskLevel::MEDIUM;
|
||||||
assessment.level = RiskLevel::MEDIUM;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Assess based on amount of change
|
// Check for TypeScript interface/type changes
|
||||||
if (our_changes > 10) {
|
if (has_typescript_interface_changes(base, ours)) {
|
||||||
|
assessment.has_api_changes = true;
|
||||||
|
assessment.risk_factors.push_back("TypeScript interface or type definitions changed");
|
||||||
|
if (assessment.level < RiskLevel::MEDIUM) {
|
||||||
|
assessment.level = RiskLevel::MEDIUM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assess based on amount of change
|
||||||
|
if (our_changes > 10) {
|
||||||
|
assessment.has_logic_changes = true;
|
||||||
|
assessment.risk_factors.push_back("Large number of changes (" + std::to_string(our_changes) + " lines)");
|
||||||
|
if (assessment.level < RiskLevel::MEDIUM) {
|
||||||
|
assessment.level = RiskLevel::MEDIUM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're discarding significant changes from theirs
|
||||||
|
if (their_changes > 5 && similarity_to_theirs < 0.3) {
|
||||||
|
assessment.risk_factors.push_back("Discarding significant changes from other branch (" +
|
||||||
|
std::to_string(their_changes) + " lines)");
|
||||||
|
if (assessment.level < RiskLevel::MEDIUM) {
|
||||||
|
assessment.level = RiskLevel::MEDIUM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate confidence score based on various factors
|
||||||
|
double change_ratio = (our_changes + their_changes) > 0 ?
|
||||||
|
static_cast<double>(our_changes) / (our_changes + their_changes) : BASE_CONFIDENCE;
|
||||||
|
assessment.confidence_score = BASE_CONFIDENCE +
|
||||||
|
(SIMILARITY_WEIGHT * similarity_to_theirs) +
|
||||||
|
(CHANGE_RATIO_WEIGHT * change_ratio);
|
||||||
|
|
||||||
|
// Add recommendations
|
||||||
|
if (assessment.level >= RiskLevel::MEDIUM) {
|
||||||
|
assessment.recommendations.push_back("Review changes carefully before accepting");
|
||||||
|
}
|
||||||
|
if (assessment.has_api_changes) {
|
||||||
|
assessment.recommendations.push_back("Verify API compatibility with dependent code");
|
||||||
|
}
|
||||||
|
if (assessment.affects_critical_section) {
|
||||||
|
assessment.recommendations.push_back("Test thoroughly, especially security and data operations");
|
||||||
|
}
|
||||||
|
if (assessment.risk_factors.empty()) {
|
||||||
|
assessment.recommendations.push_back("Changes appear safe to accept");
|
||||||
|
}
|
||||||
|
|
||||||
|
return assessment;
|
||||||
|
}
|
||||||
|
|
||||||
|
RiskAssessment analyze_risk_theirs(
|
||||||
|
const std::vector<std::string>& base,
|
||||||
|
const std::vector<std::string>& ours,
|
||||||
|
const std::vector<std::string>& theirs
|
||||||
|
) {
|
||||||
|
RiskAssessment assessment;
|
||||||
|
assessment.level = RiskLevel::LOW;
|
||||||
|
assessment.confidence_score = 0.5;
|
||||||
|
assessment.has_syntax_changes = false;
|
||||||
|
assessment.has_logic_changes = false;
|
||||||
|
assessment.has_api_changes = false;
|
||||||
|
assessment.affects_multiple_functions = false;
|
||||||
|
assessment.affects_critical_section = false;
|
||||||
|
|
||||||
|
// Calculate changes
|
||||||
|
size_t our_changes = count_changes(base, ours);
|
||||||
|
size_t their_changes = count_changes(base, theirs);
|
||||||
|
double similarity_to_ours = calculate_similarity(theirs, ours);
|
||||||
|
|
||||||
|
// Check for critical patterns
|
||||||
|
if (contains_critical_patterns(theirs)) {
|
||||||
|
assessment.affects_critical_section = true;
|
||||||
|
assessment.risk_factors.push_back("Contains critical code patterns (security/data operations)");
|
||||||
|
assessment.level = RiskLevel::HIGH;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for API changes
|
||||||
|
if (has_api_signature_changes(base, theirs)) {
|
||||||
|
assessment.has_api_changes = true;
|
||||||
|
assessment.risk_factors.push_back("Function/method signatures changed");
|
||||||
|
if (assessment.level < RiskLevel::MEDIUM) {
|
||||||
|
assessment.level = RiskLevel::MEDIUM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for TypeScript interface/type changes
|
||||||
|
if (has_typescript_interface_changes(base, theirs)) {
|
||||||
|
assessment.has_api_changes = true;
|
||||||
|
assessment.risk_factors.push_back("TypeScript interface or type definitions changed");
|
||||||
|
if (assessment.level < RiskLevel::MEDIUM) {
|
||||||
|
assessment.level = RiskLevel::MEDIUM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assess based on amount of change
|
||||||
|
if (their_changes > 10) {
|
||||||
|
assessment.has_logic_changes = true;
|
||||||
|
assessment.risk_factors.push_back("Large number of changes (" + std::to_string(their_changes) + " lines)");
|
||||||
|
if (assessment.level < RiskLevel::MEDIUM) {
|
||||||
|
assessment.level = RiskLevel::MEDIUM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're discarding our changes
|
||||||
|
if (our_changes > 5 && similarity_to_ours < 0.3) {
|
||||||
|
assessment.risk_factors.push_back("Discarding our local changes (" +
|
||||||
|
std::to_string(our_changes) + " lines)");
|
||||||
|
if (assessment.level < RiskLevel::MEDIUM) {
|
||||||
|
assessment.level = RiskLevel::MEDIUM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate confidence score
|
||||||
|
double change_ratio = (our_changes + their_changes) > 0 ?
|
||||||
|
static_cast<double>(their_changes) / (our_changes + their_changes) : BASE_CONFIDENCE;
|
||||||
|
assessment.confidence_score = BASE_CONFIDENCE +
|
||||||
|
(SIMILARITY_WEIGHT * similarity_to_ours) +
|
||||||
|
(CHANGE_RATIO_WEIGHT * change_ratio);
|
||||||
|
|
||||||
|
// Add recommendations
|
||||||
|
if (assessment.level >= RiskLevel::MEDIUM) {
|
||||||
|
assessment.recommendations.push_back("Review changes carefully before accepting");
|
||||||
|
}
|
||||||
|
if (assessment.has_api_changes) {
|
||||||
|
assessment.recommendations.push_back("Verify API compatibility with dependent code");
|
||||||
|
}
|
||||||
|
if (assessment.affects_critical_section) {
|
||||||
|
assessment.recommendations.push_back("Test thoroughly, especially security and data operations");
|
||||||
|
}
|
||||||
|
if (assessment.risk_factors.empty()) {
|
||||||
|
assessment.recommendations.push_back("Changes appear safe to accept");
|
||||||
|
}
|
||||||
|
|
||||||
|
return assessment;
|
||||||
|
}
|
||||||
|
|
||||||
|
RiskAssessment analyze_risk_both(
|
||||||
|
const std::vector<std::string>& base,
|
||||||
|
const std::vector<std::string>& ours,
|
||||||
|
const std::vector<std::string>& theirs
|
||||||
|
) {
|
||||||
|
RiskAssessment assessment;
|
||||||
|
assessment.level = RiskLevel::MEDIUM; // Default to medium for concatenation
|
||||||
|
assessment.confidence_score = 0.3; // Lower confidence for concatenation
|
||||||
|
assessment.has_syntax_changes = true;
|
||||||
assessment.has_logic_changes = true;
|
assessment.has_logic_changes = true;
|
||||||
assessment.risk_factors.push_back("Large number of changes (" +
|
assessment.has_api_changes = false;
|
||||||
std::to_string(our_changes) + " lines)");
|
assessment.affects_multiple_functions = false;
|
||||||
if (assessment.level < RiskLevel::MEDIUM) {
|
assessment.affects_critical_section = false;
|
||||||
assessment.level = RiskLevel::MEDIUM;
|
|
||||||
|
// Concatenating both versions is generally risky
|
||||||
|
assessment.risk_factors.push_back("Concatenating both versions may cause duplicates or conflicts");
|
||||||
|
|
||||||
|
// Check if either contains critical patterns
|
||||||
|
if (contains_critical_patterns(ours) || contains_critical_patterns(theirs)) {
|
||||||
|
assessment.affects_critical_section = true;
|
||||||
|
assessment.risk_factors.push_back("Contains critical code patterns that may conflict");
|
||||||
|
assessment.level = RiskLevel::HIGH;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we're discarding significant changes from theirs
|
// Check for duplicate logic
|
||||||
if (their_changes > 5 && similarity_to_theirs < 0.3) {
|
double similarity = calculate_similarity(ours, theirs);
|
||||||
assessment.risk_factors.push_back(
|
if (similarity > 0.5) {
|
||||||
"Discarding significant changes from other branch (" +
|
assessment.risk_factors.push_back("High similarity may result in duplicate code");
|
||||||
std::to_string(their_changes) + " lines)");
|
assessment.level = RiskLevel::HIGH;
|
||||||
if (assessment.level < RiskLevel::MEDIUM) {
|
|
||||||
assessment.level = RiskLevel::MEDIUM;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate confidence score based on various factors
|
// API changes from either side
|
||||||
double change_ratio =
|
if (has_api_signature_changes(base, ours) || has_api_signature_changes(base, theirs)) {
|
||||||
(our_changes + their_changes) > 0
|
assessment.has_api_changes = true;
|
||||||
? static_cast<double>(our_changes) / (our_changes + their_changes)
|
assessment.risk_factors.push_back("Multiple API changes may cause conflicts");
|
||||||
: BASE_CONFIDENCE;
|
assessment.level = RiskLevel::HIGH;
|
||||||
assessment.confidence_score = BASE_CONFIDENCE +
|
}
|
||||||
(SIMILARITY_WEIGHT * similarity_to_theirs) +
|
|
||||||
(CHANGE_RATIO_WEIGHT * change_ratio);
|
|
||||||
|
|
||||||
// Add recommendations
|
// TypeScript interface/type changes from either side
|
||||||
if (assessment.level >= RiskLevel::MEDIUM) {
|
if (has_typescript_interface_changes(base, ours) || has_typescript_interface_changes(base, theirs)) {
|
||||||
assessment.recommendations.push_back(
|
assessment.has_api_changes = true;
|
||||||
"Review changes carefully before accepting");
|
assessment.risk_factors.push_back("Multiple TypeScript interface/type changes may cause conflicts");
|
||||||
}
|
assessment.level = RiskLevel::HIGH;
|
||||||
if (assessment.has_api_changes) {
|
}
|
||||||
assessment.recommendations.push_back(
|
|
||||||
"Verify API compatibility with dependent code");
|
|
||||||
}
|
|
||||||
if (assessment.affects_critical_section) {
|
|
||||||
assessment.recommendations.push_back(
|
|
||||||
"Test thoroughly, especially security and data operations");
|
|
||||||
}
|
|
||||||
if (assessment.risk_factors.empty()) {
|
|
||||||
assessment.recommendations.push_back("Changes appear safe to accept");
|
|
||||||
}
|
|
||||||
|
|
||||||
return assessment;
|
// Recommendations for concatenation
|
||||||
|
assessment.recommendations.push_back("Manual review required - automatic concatenation is risky");
|
||||||
|
assessment.recommendations.push_back("Consider merging logic manually instead of concatenating");
|
||||||
|
assessment.recommendations.push_back("Test thoroughly for duplicate or conflicting code");
|
||||||
|
|
||||||
|
return assessment;
|
||||||
}
|
}
|
||||||
|
|
||||||
RiskAssessment analyze_risk_theirs(const std::vector<std::string> &base,
|
} // namespace analysis
|
||||||
const std::vector<std::string> &ours,
|
} // namespace wizardmerge
|
||||||
const std::vector<std::string> &theirs) {
|
|
||||||
RiskAssessment assessment;
|
|
||||||
assessment.level = RiskLevel::LOW;
|
|
||||||
assessment.confidence_score = 0.5;
|
|
||||||
assessment.has_syntax_changes = false;
|
|
||||||
assessment.has_logic_changes = false;
|
|
||||||
assessment.has_api_changes = false;
|
|
||||||
assessment.affects_multiple_functions = false;
|
|
||||||
assessment.affects_critical_section = false;
|
|
||||||
|
|
||||||
// Calculate changes
|
|
||||||
size_t our_changes = count_changes(base, ours);
|
|
||||||
size_t their_changes = count_changes(base, theirs);
|
|
||||||
double similarity_to_ours = calculate_similarity(theirs, ours);
|
|
||||||
|
|
||||||
// Check for critical patterns
|
|
||||||
if (contains_critical_patterns(theirs)) {
|
|
||||||
assessment.affects_critical_section = true;
|
|
||||||
assessment.risk_factors.push_back(
|
|
||||||
"Contains critical code patterns (security/data operations)");
|
|
||||||
assessment.level = RiskLevel::HIGH;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for API changes
|
|
||||||
if (has_api_signature_changes(base, theirs)) {
|
|
||||||
assessment.has_api_changes = true;
|
|
||||||
assessment.risk_factors.push_back("Function/method signatures changed");
|
|
||||||
if (assessment.level < RiskLevel::MEDIUM) {
|
|
||||||
assessment.level = RiskLevel::MEDIUM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for TypeScript interface/type changes
|
|
||||||
if (has_typescript_interface_changes(base, theirs)) {
|
|
||||||
assessment.has_api_changes = true;
|
|
||||||
assessment.risk_factors.push_back(
|
|
||||||
"TypeScript interface or type definitions changed");
|
|
||||||
if (assessment.level < RiskLevel::MEDIUM) {
|
|
||||||
assessment.level = RiskLevel::MEDIUM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assess based on amount of change
|
|
||||||
if (their_changes > 10) {
|
|
||||||
assessment.has_logic_changes = true;
|
|
||||||
assessment.risk_factors.push_back("Large number of changes (" +
|
|
||||||
std::to_string(their_changes) +
|
|
||||||
" lines)");
|
|
||||||
if (assessment.level < RiskLevel::MEDIUM) {
|
|
||||||
assessment.level = RiskLevel::MEDIUM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we're discarding our changes
|
|
||||||
if (our_changes > 5 && similarity_to_ours < 0.3) {
|
|
||||||
assessment.risk_factors.push_back("Discarding our local changes (" +
|
|
||||||
std::to_string(our_changes) + " lines)");
|
|
||||||
if (assessment.level < RiskLevel::MEDIUM) {
|
|
||||||
assessment.level = RiskLevel::MEDIUM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate confidence score
|
|
||||||
double change_ratio =
|
|
||||||
(our_changes + their_changes) > 0
|
|
||||||
? static_cast<double>(their_changes) / (our_changes + their_changes)
|
|
||||||
: BASE_CONFIDENCE;
|
|
||||||
assessment.confidence_score = BASE_CONFIDENCE +
|
|
||||||
(SIMILARITY_WEIGHT * similarity_to_ours) +
|
|
||||||
(CHANGE_RATIO_WEIGHT * change_ratio);
|
|
||||||
|
|
||||||
// Add recommendations
|
|
||||||
if (assessment.level >= RiskLevel::MEDIUM) {
|
|
||||||
assessment.recommendations.push_back(
|
|
||||||
"Review changes carefully before accepting");
|
|
||||||
}
|
|
||||||
if (assessment.has_api_changes) {
|
|
||||||
assessment.recommendations.push_back(
|
|
||||||
"Verify API compatibility with dependent code");
|
|
||||||
}
|
|
||||||
if (assessment.affects_critical_section) {
|
|
||||||
assessment.recommendations.push_back(
|
|
||||||
"Test thoroughly, especially security and data operations");
|
|
||||||
}
|
|
||||||
if (assessment.risk_factors.empty()) {
|
|
||||||
assessment.recommendations.push_back("Changes appear safe to accept");
|
|
||||||
}
|
|
||||||
|
|
||||||
return assessment;
|
|
||||||
}
|
|
||||||
|
|
||||||
RiskAssessment analyze_risk_both(const std::vector<std::string> &base,
|
|
||||||
const std::vector<std::string> &ours,
|
|
||||||
const std::vector<std::string> &theirs) {
|
|
||||||
RiskAssessment assessment;
|
|
||||||
assessment.level = RiskLevel::MEDIUM; // Default to medium for concatenation
|
|
||||||
assessment.confidence_score = 0.3; // Lower confidence for concatenation
|
|
||||||
assessment.has_syntax_changes = true;
|
|
||||||
assessment.has_logic_changes = true;
|
|
||||||
assessment.has_api_changes = false;
|
|
||||||
assessment.affects_multiple_functions = false;
|
|
||||||
assessment.affects_critical_section = false;
|
|
||||||
|
|
||||||
// Concatenating both versions is generally risky
|
|
||||||
assessment.risk_factors.push_back(
|
|
||||||
"Concatenating both versions may cause duplicates or conflicts");
|
|
||||||
|
|
||||||
// Check if either contains critical patterns
|
|
||||||
if (contains_critical_patterns(ours) || contains_critical_patterns(theirs)) {
|
|
||||||
assessment.affects_critical_section = true;
|
|
||||||
assessment.risk_factors.push_back(
|
|
||||||
"Contains critical code patterns that may conflict");
|
|
||||||
assessment.level = RiskLevel::HIGH;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate logic
|
|
||||||
double similarity = calculate_similarity(ours, theirs);
|
|
||||||
if (similarity > 0.5) {
|
|
||||||
assessment.risk_factors.push_back(
|
|
||||||
"High similarity may result in duplicate code");
|
|
||||||
assessment.level = RiskLevel::HIGH;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API changes from either side
|
|
||||||
if (has_api_signature_changes(base, ours) ||
|
|
||||||
has_api_signature_changes(base, theirs)) {
|
|
||||||
assessment.has_api_changes = true;
|
|
||||||
assessment.risk_factors.push_back(
|
|
||||||
"Multiple API changes may cause conflicts");
|
|
||||||
assessment.level = RiskLevel::HIGH;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypeScript interface/type changes from either side
|
|
||||||
if (has_typescript_interface_changes(base, ours) ||
|
|
||||||
has_typescript_interface_changes(base, theirs)) {
|
|
||||||
assessment.has_api_changes = true;
|
|
||||||
assessment.risk_factors.push_back(
|
|
||||||
"Multiple TypeScript interface/type changes may cause conflicts");
|
|
||||||
assessment.level = RiskLevel::HIGH;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recommendations for concatenation
|
|
||||||
assessment.recommendations.push_back(
|
|
||||||
"Manual review required - automatic concatenation is risky");
|
|
||||||
assessment.recommendations.push_back(
|
|
||||||
"Consider merging logic manually instead of concatenating");
|
|
||||||
assessment.recommendations.push_back(
|
|
||||||
"Test thoroughly for duplicate or conflicting code");
|
|
||||||
|
|
||||||
return assessment;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace analysis
|
|
||||||
} // namespace wizardmerge
|
|
||||||
|
|||||||
@@ -17,33 +17,33 @@ namespace controllers {
|
|||||||
* @brief HTTP controller for three-way merge API
|
* @brief HTTP controller for three-way merge API
|
||||||
*/
|
*/
|
||||||
class MergeController : public HttpController<MergeController> {
|
class MergeController : public HttpController<MergeController> {
|
||||||
public:
|
public:
|
||||||
METHOD_LIST_BEGIN
|
METHOD_LIST_BEGIN
|
||||||
// POST /api/merge - Perform three-way merge
|
// POST /api/merge - Perform three-way merge
|
||||||
ADD_METHOD_TO(MergeController::merge, "/api/merge", Post);
|
ADD_METHOD_TO(MergeController::merge, "/api/merge", Post);
|
||||||
METHOD_LIST_END
|
METHOD_LIST_END
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Perform three-way merge operation
|
* @brief Perform three-way merge operation
|
||||||
*
|
*
|
||||||
* Request body should be JSON:
|
* Request body should be JSON:
|
||||||
* {
|
* {
|
||||||
* "base": ["line1", "line2", ...],
|
* "base": ["line1", "line2", ...],
|
||||||
* "ours": ["line1", "line2", ...],
|
* "ours": ["line1", "line2", ...],
|
||||||
* "theirs": ["line1", "line2", ...]
|
* "theirs": ["line1", "line2", ...]
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* Response:
|
* Response:
|
||||||
* {
|
* {
|
||||||
* "merged": ["line1", "line2", ...],
|
* "merged": ["line1", "line2", ...],
|
||||||
* "conflicts": [...]
|
* "conflicts": [...]
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
void merge(const HttpRequestPtr &req,
|
void merge(const HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace controllers
|
} // namespace controllers
|
||||||
} // namespace wizardmerge
|
} // namespace wizardmerge
|
||||||
|
|
||||||
#endif // WIZARDMERGE_CONTROLLERS_MERGE_CONTROLLER_H
|
#endif // WIZARDMERGE_CONTROLLERS_MERGE_CONTROLLER_H
|
||||||
|
|||||||
@@ -17,49 +17,49 @@ namespace controllers {
|
|||||||
* @brief HTTP controller for pull request merge API
|
* @brief HTTP controller for pull request merge API
|
||||||
*/
|
*/
|
||||||
class PRController : public HttpController<PRController> {
|
class PRController : public HttpController<PRController> {
|
||||||
public:
|
public:
|
||||||
METHOD_LIST_BEGIN
|
METHOD_LIST_BEGIN
|
||||||
// POST /api/pr/resolve - Resolve conflicts in a pull request
|
// POST /api/pr/resolve - Resolve conflicts in a pull request
|
||||||
ADD_METHOD_TO(PRController::resolvePR, "/api/pr/resolve", Post);
|
ADD_METHOD_TO(PRController::resolvePR, "/api/pr/resolve", Post);
|
||||||
METHOD_LIST_END
|
METHOD_LIST_END
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Resolve merge conflicts in a pull request
|
* @brief Resolve merge conflicts in a pull request
|
||||||
*
|
*
|
||||||
* Request body should be JSON:
|
* Request body should be JSON:
|
||||||
* {
|
* {
|
||||||
* "pr_url": "https://github.com/owner/repo/pull/123",
|
* "pr_url": "https://github.com/owner/repo/pull/123",
|
||||||
* "github_token": "optional_github_token",
|
* "github_token": "optional_github_token",
|
||||||
* "create_branch": true,
|
* "create_branch": true,
|
||||||
* "branch_name": "wizardmerge-resolved-pr-123"
|
* "branch_name": "wizardmerge-resolved-pr-123"
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* Response:
|
* Response:
|
||||||
* {
|
* {
|
||||||
* "success": true,
|
* "success": true,
|
||||||
* "pr_info": {
|
* "pr_info": {
|
||||||
* "number": 123,
|
* "number": 123,
|
||||||
* "title": "...",
|
* "title": "...",
|
||||||
* "base_ref": "main",
|
* "base_ref": "main",
|
||||||
* "head_ref": "feature-branch"
|
* "head_ref": "feature-branch"
|
||||||
* },
|
* },
|
||||||
* "resolved_files": [
|
* "resolved_files": [
|
||||||
* {
|
* {
|
||||||
* "filename": "...",
|
* "filename": "...",
|
||||||
* "had_conflicts": true,
|
* "had_conflicts": true,
|
||||||
* "auto_resolved": true,
|
* "auto_resolved": true,
|
||||||
* "merged_content": ["line1", "line2", ...]
|
* "merged_content": ["line1", "line2", ...]
|
||||||
* }
|
* }
|
||||||
* ],
|
* ],
|
||||||
* "branch_created": true,
|
* "branch_created": true,
|
||||||
* "branch_name": "wizardmerge-resolved-pr-123"
|
* "branch_name": "wizardmerge-resolved-pr-123"
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
void resolvePR(const HttpRequestPtr &req,
|
void resolvePR(const HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace controllers
|
} // namespace controllers
|
||||||
} // namespace wizardmerge
|
} // namespace wizardmerge
|
||||||
|
|
||||||
#endif // WIZARDMERGE_CONTROLLERS_PR_CONTROLLER_H
|
#endif // WIZARDMERGE_CONTROLLERS_PR_CONTROLLER_H
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "wizardmerge/git/git_cli.h"
|
#include "wizardmerge/git/git_cli.h"
|
||||||
#include <array>
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <filesystem>
|
#include <array>
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <filesystem>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
|
|
||||||
namespace wizardmerge {
|
namespace wizardmerge {
|
||||||
@@ -19,209 +19,222 @@ namespace {
|
|||||||
/**
|
/**
|
||||||
* @brief Execute a shell command and capture output
|
* @brief Execute a shell command and capture output
|
||||||
*/
|
*/
|
||||||
GitResult execute_command(const std::string &command) {
|
GitResult execute_command(const std::string& command) {
|
||||||
GitResult result;
|
GitResult result;
|
||||||
result.exit_code = 0;
|
result.exit_code = 0;
|
||||||
|
|
||||||
// Execute command and capture output
|
// Execute command and capture output
|
||||||
std::array<char, 128> buffer;
|
std::array<char, 128> buffer;
|
||||||
std::string output;
|
std::string output;
|
||||||
|
|
||||||
|
FILE* pipe = popen((command + " 2>&1").c_str(), "r");
|
||||||
|
if (!pipe) {
|
||||||
|
result.success = false;
|
||||||
|
result.error = "Failed to execute command";
|
||||||
|
result.exit_code = -1;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
|
||||||
|
output += buffer.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = pclose(pipe);
|
||||||
|
result.exit_code = WEXITSTATUS(status);
|
||||||
|
result.success = (result.exit_code == 0);
|
||||||
|
result.output = output;
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
result.error = output;
|
||||||
|
}
|
||||||
|
|
||||||
FILE *pipe = popen((command + " 2>&1").c_str(), "r");
|
|
||||||
if (!pipe) {
|
|
||||||
result.success = false;
|
|
||||||
result.error = "Failed to execute command";
|
|
||||||
result.exit_code = -1;
|
|
||||||
return result;
|
return result;
|
||||||
}
|
|
||||||
|
|
||||||
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
|
|
||||||
output += buffer.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
int status = pclose(pipe);
|
|
||||||
result.exit_code = WEXITSTATUS(status);
|
|
||||||
result.success = (result.exit_code == 0);
|
|
||||||
result.output = output;
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
result.error = output;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Build git command with working directory
|
* @brief Build git command with working directory
|
||||||
*/
|
*/
|
||||||
std::string git_command(const std::string &repo_path, const std::string &cmd) {
|
std::string git_command(const std::string& repo_path, const std::string& cmd) {
|
||||||
if (repo_path.empty()) {
|
if (repo_path.empty()) {
|
||||||
return "git " + cmd;
|
return "git " + cmd;
|
||||||
}
|
}
|
||||||
return "git -C \"" + repo_path + "\" " + cmd;
|
return "git -C \"" + repo_path + "\" " + cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
bool is_git_available() {
|
bool is_git_available() {
|
||||||
GitResult result = execute_command("git --version");
|
GitResult result = execute_command("git --version");
|
||||||
return result.success;
|
return result.success;
|
||||||
}
|
}
|
||||||
|
|
||||||
GitResult clone_repository(const std::string &url,
|
GitResult clone_repository(
|
||||||
const std::string &destination,
|
const std::string& url,
|
||||||
const std::string &branch, int depth) {
|
const std::string& destination,
|
||||||
std::ostringstream cmd;
|
const std::string& branch,
|
||||||
cmd << "git clone";
|
int depth
|
||||||
|
) {
|
||||||
|
std::ostringstream cmd;
|
||||||
|
cmd << "git clone";
|
||||||
|
|
||||||
if (!branch.empty()) {
|
if (!branch.empty()) {
|
||||||
cmd << " --branch \"" << branch << "\"";
|
cmd << " --branch \"" << branch << "\"";
|
||||||
}
|
|
||||||
|
|
||||||
if (depth > 0) {
|
|
||||||
cmd << " --depth " << depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd << " \"" << url << "\" \"" << destination << "\"";
|
|
||||||
|
|
||||||
return execute_command(cmd.str());
|
|
||||||
}
|
|
||||||
|
|
||||||
GitResult create_branch(const std::string &repo_path,
|
|
||||||
const std::string &branch_name,
|
|
||||||
const std::string &base_branch) {
|
|
||||||
std::ostringstream cmd;
|
|
||||||
cmd << "checkout -b \"" << branch_name << "\"";
|
|
||||||
|
|
||||||
if (!base_branch.empty()) {
|
|
||||||
cmd << " \"" << base_branch << "\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
return execute_command(git_command(repo_path, cmd.str()));
|
|
||||||
}
|
|
||||||
|
|
||||||
GitResult checkout_branch(const std::string &repo_path,
|
|
||||||
const std::string &branch_name) {
|
|
||||||
std::string cmd = "checkout \"" + branch_name + "\"";
|
|
||||||
return execute_command(git_command(repo_path, cmd));
|
|
||||||
}
|
|
||||||
|
|
||||||
GitResult add_files(const std::string &repo_path,
|
|
||||||
const std::vector<std::string> &files) {
|
|
||||||
if (files.empty()) {
|
|
||||||
GitResult result;
|
|
||||||
result.success = true;
|
|
||||||
result.output = "No files to add";
|
|
||||||
result.exit_code = 0;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ostringstream cmd;
|
|
||||||
cmd << "add";
|
|
||||||
|
|
||||||
for (const auto &file : files) {
|
|
||||||
cmd << " \"" << file << "\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
return execute_command(git_command(repo_path, cmd.str()));
|
|
||||||
}
|
|
||||||
|
|
||||||
GitResult commit(const std::string &repo_path, const std::string &message,
|
|
||||||
const GitConfig &config) {
|
|
||||||
// Set user config if provided
|
|
||||||
if (!config.user_name.empty() && !config.user_email.empty()) {
|
|
||||||
auto name_result = execute_command(git_command(
|
|
||||||
repo_path, "config user.name \"" + config.user_name + "\""));
|
|
||||||
if (!name_result.success) {
|
|
||||||
GitResult result;
|
|
||||||
result.success = false;
|
|
||||||
result.error = "Failed to set user.name: " + name_result.error;
|
|
||||||
result.exit_code = name_result.exit_code;
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto email_result = execute_command(git_command(
|
if (depth > 0) {
|
||||||
repo_path, "config user.email \"" + config.user_email + "\""));
|
cmd << " --depth " << depth;
|
||||||
if (!email_result.success) {
|
|
||||||
GitResult result;
|
|
||||||
result.success = false;
|
|
||||||
result.error = "Failed to set user.email: " + email_result.error;
|
|
||||||
result.exit_code = email_result.exit_code;
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Escape commit message for shell
|
cmd << " \"" << url << "\" \"" << destination << "\"";
|
||||||
std::string escaped_message = message;
|
|
||||||
size_t pos = 0;
|
|
||||||
while ((pos = escaped_message.find("\"", pos)) != std::string::npos) {
|
|
||||||
escaped_message.replace(pos, 1, "\\\"");
|
|
||||||
pos += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string cmd = "commit -m \"" + escaped_message + "\"";
|
return execute_command(cmd.str());
|
||||||
return execute_command(git_command(repo_path, cmd));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GitResult push(const std::string &repo_path, const std::string &remote,
|
GitResult create_branch(
|
||||||
const std::string &branch, bool force, const GitConfig &config) {
|
const std::string& repo_path,
|
||||||
std::ostringstream cmd;
|
const std::string& branch_name,
|
||||||
cmd << "push";
|
const std::string& base_branch
|
||||||
|
) {
|
||||||
|
std::ostringstream cmd;
|
||||||
|
cmd << "checkout -b \"" << branch_name << "\"";
|
||||||
|
|
||||||
if (force) {
|
if (!base_branch.empty()) {
|
||||||
cmd << " --force";
|
cmd << " \"" << base_branch << "\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set upstream if it's a new branch
|
return execute_command(git_command(repo_path, cmd.str()));
|
||||||
cmd << " --set-upstream \"" << remote << "\" \"" << branch << "\"";
|
|
||||||
|
|
||||||
std::string full_cmd = git_command(repo_path, cmd.str());
|
|
||||||
|
|
||||||
// If auth token is provided, inject it into the URL
|
|
||||||
// This is a simplified approach; in production, use credential helpers
|
|
||||||
if (!config.auth_token.empty()) {
|
|
||||||
// Note: This assumes HTTPS URLs. For production, use git credential helpers
|
|
||||||
// or SSH keys for better security
|
|
||||||
std::cerr << "Note: Auth token provided. Consider using credential helpers "
|
|
||||||
"for production."
|
|
||||||
<< std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return execute_command(full_cmd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> get_current_branch(const std::string &repo_path) {
|
GitResult checkout_branch(
|
||||||
GitResult result =
|
const std::string& repo_path,
|
||||||
execute_command(git_command(repo_path, "rev-parse --abbrev-ref HEAD"));
|
const std::string& branch_name
|
||||||
|
) {
|
||||||
if (!result.success) {
|
std::string cmd = "checkout \"" + branch_name + "\"";
|
||||||
return std::nullopt;
|
return execute_command(git_command(repo_path, cmd));
|
||||||
}
|
|
||||||
|
|
||||||
// Trim whitespace
|
|
||||||
std::string branch = result.output;
|
|
||||||
size_t last_non_ws = branch.find_last_not_of(" \n\r\t");
|
|
||||||
|
|
||||||
if (last_non_ws == std::string::npos) {
|
|
||||||
// String contains only whitespace
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
branch.erase(last_non_ws + 1);
|
|
||||||
|
|
||||||
return branch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool branch_exists(const std::string &repo_path,
|
GitResult add_files(
|
||||||
const std::string &branch_name) {
|
const std::string& repo_path,
|
||||||
std::string cmd = "rev-parse --verify \"" + branch_name + "\"";
|
const std::vector<std::string>& files
|
||||||
GitResult result = execute_command(git_command(repo_path, cmd));
|
) {
|
||||||
return result.success;
|
if (files.empty()) {
|
||||||
|
GitResult result;
|
||||||
|
result.success = true;
|
||||||
|
result.output = "No files to add";
|
||||||
|
result.exit_code = 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream cmd;
|
||||||
|
cmd << "add";
|
||||||
|
|
||||||
|
for (const auto& file : files) {
|
||||||
|
cmd << " \"" << file << "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
return execute_command(git_command(repo_path, cmd.str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
GitResult status(const std::string &repo_path) {
|
GitResult commit(
|
||||||
return execute_command(git_command(repo_path, "status"));
|
const std::string& repo_path,
|
||||||
|
const std::string& message,
|
||||||
|
const GitConfig& config
|
||||||
|
) {
|
||||||
|
// Set user config if provided
|
||||||
|
if (!config.user_name.empty() && !config.user_email.empty()) {
|
||||||
|
auto name_result = execute_command(git_command(repo_path,
|
||||||
|
"config user.name \"" + config.user_name + "\""));
|
||||||
|
if (!name_result.success) {
|
||||||
|
GitResult result;
|
||||||
|
result.success = false;
|
||||||
|
result.error = "Failed to set user.name: " + name_result.error;
|
||||||
|
result.exit_code = name_result.exit_code;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto email_result = execute_command(git_command(repo_path,
|
||||||
|
"config user.email \"" + config.user_email + "\""));
|
||||||
|
if (!email_result.success) {
|
||||||
|
GitResult result;
|
||||||
|
result.success = false;
|
||||||
|
result.error = "Failed to set user.email: " + email_result.error;
|
||||||
|
result.exit_code = email_result.exit_code;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape commit message for shell
|
||||||
|
std::string escaped_message = message;
|
||||||
|
size_t pos = 0;
|
||||||
|
while ((pos = escaped_message.find("\"", pos)) != std::string::npos) {
|
||||||
|
escaped_message.replace(pos, 1, "\\\"");
|
||||||
|
pos += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd = "commit -m \"" + escaped_message + "\"";
|
||||||
|
return execute_command(git_command(repo_path, cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace git
|
GitResult push(
|
||||||
} // namespace wizardmerge
|
const std::string& repo_path,
|
||||||
|
const std::string& remote,
|
||||||
|
const std::string& branch,
|
||||||
|
bool force,
|
||||||
|
const GitConfig& config
|
||||||
|
) {
|
||||||
|
std::ostringstream cmd;
|
||||||
|
cmd << "push";
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
cmd << " --force";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set upstream if it's a new branch
|
||||||
|
cmd << " --set-upstream \"" << remote << "\" \"" << branch << "\"";
|
||||||
|
|
||||||
|
std::string full_cmd = git_command(repo_path, cmd.str());
|
||||||
|
|
||||||
|
// If auth token is provided, inject it into the URL
|
||||||
|
// This is a simplified approach; in production, use credential helpers
|
||||||
|
if (!config.auth_token.empty()) {
|
||||||
|
// Note: This assumes HTTPS URLs. For production, use git credential helpers
|
||||||
|
// or SSH keys for better security
|
||||||
|
std::cerr << "Note: Auth token provided. Consider using credential helpers for production." << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return execute_command(full_cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> get_current_branch(const std::string& repo_path) {
|
||||||
|
GitResult result = execute_command(git_command(repo_path, "rev-parse --abbrev-ref HEAD"));
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim whitespace
|
||||||
|
std::string branch = result.output;
|
||||||
|
size_t last_non_ws = branch.find_last_not_of(" \n\r\t");
|
||||||
|
|
||||||
|
if (last_non_ws == std::string::npos) {
|
||||||
|
// String contains only whitespace
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
branch.erase(last_non_ws + 1);
|
||||||
|
|
||||||
|
return branch;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool branch_exists(const std::string& repo_path, const std::string& branch_name) {
|
||||||
|
std::string cmd = "rev-parse --verify \"" + branch_name + "\"";
|
||||||
|
GitResult result = execute_command(git_command(repo_path, cmd));
|
||||||
|
return result.success;
|
||||||
|
}
|
||||||
|
|
||||||
|
GitResult status(const std::string& repo_path) {
|
||||||
|
return execute_command(git_command(repo_path, "status"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace git
|
||||||
|
} // namespace wizardmerge
|
||||||
|
|||||||
@@ -4,432 +4,512 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#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 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
static const std::string base64_chars =
|
||||||
"abcdefghijklmnopqrstuvwxyz"
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
"0123456789+/";
|
"abcdefghijklmnopqrstuvwxyz"
|
||||||
|
"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++)
|
for (int i = 0; i < 64; i++) T[base64_chars[i]] = 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)
|
if (T[c] == -1) break;
|
||||||
break;
|
val = (val << 6) + T[c];
|
||||||
val = (val << 6) + T[c];
|
valb += 6;
|
||||||
valb += 6;
|
if (valb >= 0) {
|
||||||
if (valb >= 0) {
|
decoded.push_back(char((val >> valb) & 0xFF));
|
||||||
decoded.push_back(char((val >> valb) & 0xFF));
|
valb -= 8;
|
||||||
valb -= 8;
|
}
|
||||||
}
|
}
|
||||||
}
|
return decoded;
|
||||||
return decoded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback for libcurl to write response data
|
// Callback for libcurl to write response data
|
||||||
size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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,
|
bool http_get(const std::string& url, const std::string& token, std::string& response, GitPlatform platform = GitPlatform::GitHub) {
|
||||||
std::string &response,
|
CURL* curl = curl_easy_init();
|
||||||
GitPlatform platform = GitPlatform::GitHub) {
|
if (!curl) {
|
||||||
CURL *curl = curl_easy_init();
|
std::cerr << "Failed to initialize CURL" << std::endl;
|
||||||
if (!curl) {
|
return false;
|
||||||
std::cerr << "Failed to initialize CURL" << std::endl;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.clear();
|
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "WizardMerge/1.0");
|
|
||||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
|
||||||
|
|
||||||
// Setup headers based on platform
|
|
||||||
struct curl_slist *headers = nullptr;
|
|
||||||
|
|
||||||
if (platform == GitPlatform::GitHub) {
|
|
||||||
headers =
|
|
||||||
curl_slist_append(headers, "Accept: application/vnd.github.v3+json");
|
|
||||||
if (!token.empty()) {
|
|
||||||
std::string auth_header = "Authorization: token " + token;
|
|
||||||
headers = curl_slist_append(headers, auth_header.c_str());
|
|
||||||
}
|
}
|
||||||
} else if (platform == GitPlatform::GitLab) {
|
|
||||||
headers = curl_slist_append(headers, "Accept: application/json");
|
response.clear();
|
||||||
if (!token.empty()) {
|
|
||||||
std::string auth_header = "PRIVATE-TOKEN: " + token;
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||||
headers = curl_slist_append(headers, auth_header.c_str());
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "WizardMerge/1.0");
|
||||||
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
||||||
|
|
||||||
|
// Setup headers based on platform
|
||||||
|
struct curl_slist* headers = nullptr;
|
||||||
|
|
||||||
|
if (platform == GitPlatform::GitHub) {
|
||||||
|
headers = curl_slist_append(headers, "Accept: application/vnd.github.v3+json");
|
||||||
|
if (!token.empty()) {
|
||||||
|
std::string auth_header = "Authorization: token " + token;
|
||||||
|
headers = curl_slist_append(headers, auth_header.c_str());
|
||||||
|
}
|
||||||
|
} else if (platform == GitPlatform::GitLab) {
|
||||||
|
headers = curl_slist_append(headers, "Accept: application/json");
|
||||||
|
if (!token.empty()) {
|
||||||
|
std::string auth_header = "PRIVATE-TOKEN: " + token;
|
||||||
|
headers = curl_slist_append(headers, auth_header.c_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
|
|
||||||
CURLcode res = curl_easy_perform(curl);
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
|
||||||
bool success = (res == CURLE_OK);
|
bool success = (res == CURLE_OK);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
std::cerr << "CURL error: " << curl_easy_strerror(res) << std::endl;
|
std::cerr << "CURL error: " << curl_easy_strerror(res) << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
curl_slist_free_all(headers);
|
curl_slist_free_all(headers);
|
||||||
curl_easy_cleanup(curl);
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Split string by newlines
|
* @brief Split string by newlines
|
||||||
*/
|
*/
|
||||||
std::vector<std::string> split_lines(const std::string &content) {
|
std::vector<std::string> split_lines(const std::string& content) {
|
||||||
std::vector<std::string> lines;
|
std::vector<std::string> lines;
|
||||||
std::istringstream stream(content);
|
std::istringstream stream(content);
|
||||||
std::string line;
|
std::string line;
|
||||||
|
|
||||||
while (std::getline(stream, line)) {
|
while (std::getline(stream, line)) {
|
||||||
lines.push_back(line);
|
lines.push_back(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
bool parse_pr_url(const std::string &url, GitPlatform &platform,
|
bool parse_pr_url(const std::string& url, GitPlatform& platform,
|
||||||
std::string &owner, std::string &repo, int &pr_number) {
|
std::string& owner, std::string& repo, int& pr_number) {
|
||||||
// Try GitHub pattern first:
|
// Try GitHub pattern first:
|
||||||
// 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(
|
std::regex github_regex(R"((?:https?://)?(?:www\.)?github\.com/([^/]+)/([^/]+)/pull/(\d+))");
|
||||||
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)) {
|
||||||
if (matches.size() == 4) {
|
if (matches.size() == 4) {
|
||||||
platform = GitPlatform::GitHub;
|
platform = GitPlatform::GitHub;
|
||||||
owner = matches[1].str();
|
owner = matches[1].str();
|
||||||
repo = matches[2].str();
|
repo = matches[2].str();
|
||||||
pr_number = std::stoi(matches[3].str());
|
pr_number = std::stoi(matches[3].str());
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Try GitLab pattern:
|
// Try GitLab pattern:
|
||||||
// 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(
|
std::regex gitlab_regex(R"((?:https?://)?(?:www\.)?gitlab\.com/([^/-]+(?:/[^/-]+)*?)/-/merge_requests/(\d+))");
|
||||||
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) {
|
||||||
platform = GitPlatform::GitLab;
|
platform = GitPlatform::GitLab;
|
||||||
std::string full_path = matches[1].str();
|
std::string full_path = matches[1].str();
|
||||||
|
|
||||||
// For GitLab, store the full project path
|
// For GitLab, store the full project path
|
||||||
// The path can be: owner/repo or group/subgroup/project
|
// The path can be: owner/repo or group/subgroup/project
|
||||||
// We split at the last slash to separate for potential use
|
// We split at the last slash to separate for potential use
|
||||||
size_t last_slash = full_path.find_last_of('/');
|
size_t last_slash = full_path.find_last_of('/');
|
||||||
if (last_slash != std::string::npos) {
|
if (last_slash != std::string::npos) {
|
||||||
owner = full_path.substr(0, last_slash);
|
owner = full_path.substr(0, last_slash);
|
||||||
repo = full_path.substr(last_slash + 1);
|
repo = full_path.substr(last_slash + 1);
|
||||||
} else {
|
} else {
|
||||||
// Single level project (rare but possible)
|
// Single level project (rare but possible)
|
||||||
// Store entire path as owner, repo empty
|
// Store entire path as owner, repo empty
|
||||||
// API calls will use full path by concatenating
|
// API calls will use full path by concatenating
|
||||||
owner = full_path;
|
owner = full_path;
|
||||||
repo = "";
|
repo = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
pr_number = std::stoi(matches[2].str());
|
pr_number = std::stoi(matches[2].str());
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
platform = GitPlatform::Unknown;
|
platform = GitPlatform::Unknown;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<PullRequest> fetch_pull_request(GitPlatform platform,
|
std::optional<PullRequest> fetch_pull_request(
|
||||||
const std::string &owner,
|
GitPlatform platform,
|
||||||
const std::string &repo,
|
const std::string& owner,
|
||||||
int pr_number,
|
const std::string& repo,
|
||||||
const std::string &token) {
|
int pr_number,
|
||||||
PullRequest pr;
|
const std::string& token
|
||||||
pr.platform = platform;
|
) {
|
||||||
pr.number = pr_number;
|
PullRequest pr;
|
||||||
pr.repo_owner = owner;
|
pr.platform = platform;
|
||||||
pr.repo_name = repo;
|
pr.number = pr_number;
|
||||||
|
pr.repo_owner = owner;
|
||||||
|
pr.repo_name = repo;
|
||||||
|
|
||||||
std::string pr_url, files_url;
|
std::string pr_url, files_url;
|
||||||
|
|
||||||
if (platform == GitPlatform::GitHub) {
|
if (platform == GitPlatform::GitHub) {
|
||||||
// GitHub API URLs
|
// GitHub API URLs
|
||||||
pr_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" +
|
pr_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + std::to_string(pr_number);
|
||||||
std::to_string(pr_number);
|
files_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + std::to_string(pr_number) + "/files";
|
||||||
files_url = "https://api.github.com/repos/" + owner + "/" + repo +
|
} else if (platform == GitPlatform::GitLab) {
|
||||||
"/pulls/" + std::to_string(pr_number) + "/files";
|
// GitLab API URLs - encode project path
|
||||||
} else if (platform == GitPlatform::GitLab) {
|
std::string project_path = owner;
|
||||||
// GitLab API URLs - encode project path
|
if (!repo.empty()) {
|
||||||
std::string project_path = owner;
|
project_path += "/" + repo;
|
||||||
if (!repo.empty()) {
|
}
|
||||||
project_path += "/" + repo;
|
// URL encode the project path
|
||||||
}
|
CURL* curl = curl_easy_init();
|
||||||
// URL encode the project path
|
char* encoded = curl_easy_escape(curl, project_path.c_str(), project_path.length());
|
||||||
CURL *curl = curl_easy_init();
|
std::string encoded_project = encoded;
|
||||||
char *encoded =
|
curl_free(encoded);
|
||||||
curl_easy_escape(curl, project_path.c_str(), project_path.length());
|
curl_easy_cleanup(curl);
|
||||||
std::string encoded_project = encoded;
|
|
||||||
curl_free(encoded);
|
|
||||||
curl_easy_cleanup(curl);
|
|
||||||
|
|
||||||
pr_url = "https://gitlab.com/api/v4/projects/" + encoded_project +
|
pr_url = "https://gitlab.com/api/v4/projects/" + encoded_project + "/merge_requests/" + std::to_string(pr_number);
|
||||||
"/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";
|
||||||
files_url = "https://gitlab.com/api/v4/projects/" + encoded_project +
|
} else {
|
||||||
"/merge_requests/" + std::to_string(pr_number) + "/changes";
|
std::cerr << "Unknown platform" << std::endl;
|
||||||
} else {
|
return std::nullopt;
|
||||||
std::cerr << "Unknown platform" << std::endl;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch PR/MR info
|
|
||||||
std::string response;
|
|
||||||
if (!http_get(pr_url, token, response, platform)) {
|
|
||||||
std::cerr << "Failed to fetch pull/merge request info" << std::endl;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse JSON response
|
|
||||||
Json::Value root;
|
|
||||||
Json::CharReaderBuilder reader;
|
|
||||||
std::string errs;
|
|
||||||
std::istringstream s(response);
|
|
||||||
|
|
||||||
if (!Json::parseFromStream(reader, s, &root, &errs)) {
|
|
||||||
std::cerr << "Failed to parse PR/MR JSON: " << errs << std::endl;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
pr.title = root.get("title", "").asString();
|
|
||||||
pr.state = root.get("state", "").asString();
|
|
||||||
|
|
||||||
if (platform == GitPlatform::GitHub) {
|
|
||||||
if (root.isMember("base") && root["base"].isObject()) {
|
|
||||||
pr.base_ref = root["base"].get("ref", "").asString();
|
|
||||||
pr.base_sha = root["base"].get("sha", "").asString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.isMember("head") && root["head"].isObject()) {
|
// Fetch PR/MR info
|
||||||
pr.head_ref = root["head"].get("ref", "").asString();
|
std::string response;
|
||||||
pr.head_sha = root["head"].get("sha", "").asString();
|
if (!http_get(pr_url, token, response, platform)) {
|
||||||
|
std::cerr << "Failed to fetch pull/merge request info" << std::endl;
|
||||||
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
pr.mergeable = root.get("mergeable", false).asBool();
|
pr.title = extract_string_field(response, "title");
|
||||||
pr.mergeable_state = root.get("mergeable_state", "unknown").asString();
|
pr.state = extract_string_field(response, "state");
|
||||||
} else if (platform == GitPlatform::GitLab) {
|
|
||||||
pr.base_ref = root.get("target_branch", "").asString();
|
|
||||||
pr.head_ref = root.get("source_branch", "").asString();
|
|
||||||
pr.base_sha =
|
|
||||||
root.get("diff_refs", Json::Value::null).get("base_sha", "").asString();
|
|
||||||
pr.head_sha =
|
|
||||||
root.get("diff_refs", Json::Value::null).get("head_sha", "").asString();
|
|
||||||
|
|
||||||
// GitLab uses different merge status
|
if (platform == GitPlatform::GitHub) {
|
||||||
std::string merge_status = root.get("merge_status", "").asString();
|
if (auto base_object = extract_object_segment(response, "base")) {
|
||||||
pr.mergeable = (merge_status == "can_be_merged");
|
pr.base_ref = extract_string_field(*base_object, "ref");
|
||||||
pr.mergeable_state = merge_status;
|
pr.base_sha = extract_string_field(*base_object, "sha");
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch PR/MR files
|
|
||||||
std::string files_response;
|
|
||||||
|
|
||||||
if (!http_get(files_url, token, files_response, platform)) {
|
|
||||||
std::cerr << "Failed to fetch pull/merge request files" << std::endl;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
Json::Value files_root;
|
|
||||||
std::istringstream files_stream(files_response);
|
|
||||||
|
|
||||||
if (!Json::parseFromStream(reader, files_stream, &files_root, &errs)) {
|
|
||||||
std::cerr << "Failed to parse files JSON: " << errs << std::endl;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process files based on platform
|
|
||||||
if (platform == GitPlatform::GitHub && files_root.isArray()) {
|
|
||||||
// GitHub format: array of file objects
|
|
||||||
for (const auto &file : files_root) {
|
|
||||||
PRFile pr_file;
|
|
||||||
pr_file.filename = file.get("filename", "").asString();
|
|
||||||
pr_file.status = file.get("status", "").asString();
|
|
||||||
pr_file.additions = file.get("additions", 0).asInt();
|
|
||||||
pr_file.deletions = file.get("deletions", 0).asInt();
|
|
||||||
pr_file.changes = file.get("changes", 0).asInt();
|
|
||||||
|
|
||||||
pr.files.push_back(pr_file);
|
|
||||||
}
|
|
||||||
} else if (platform == GitPlatform::GitLab &&
|
|
||||||
files_root.isMember("changes")) {
|
|
||||||
// GitLab format: object with "changes" array
|
|
||||||
const Json::Value &changes = files_root["changes"];
|
|
||||||
if (changes.isArray()) {
|
|
||||||
for (const auto &file : changes) {
|
|
||||||
PRFile pr_file;
|
|
||||||
pr_file.filename =
|
|
||||||
file.get("new_path", file.get("old_path", "").asString())
|
|
||||||
.asString();
|
|
||||||
|
|
||||||
// Determine status from new_file, deleted_file, renamed_file flags
|
|
||||||
bool new_file = file.get("new_file", false).asBool();
|
|
||||||
bool deleted_file = file.get("deleted_file", false).asBool();
|
|
||||||
bool renamed_file = file.get("renamed_file", false).asBool();
|
|
||||||
|
|
||||||
if (new_file) {
|
|
||||||
pr_file.status = "added";
|
|
||||||
} else if (deleted_file) {
|
|
||||||
pr_file.status = "removed";
|
|
||||||
} else if (renamed_file) {
|
|
||||||
pr_file.status = "renamed";
|
|
||||||
} else {
|
|
||||||
pr_file.status = "modified";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitLab doesn't provide addition/deletion counts in the changes
|
if (auto head_object = extract_object_segment(response, "head")) {
|
||||||
// endpoint
|
pr.head_ref = extract_string_field(*head_object, "ref");
|
||||||
pr_file.additions = 0;
|
pr.head_sha = extract_string_field(*head_object, "sha");
|
||||||
pr_file.deletions = 0;
|
}
|
||||||
pr_file.changes = 0;
|
|
||||||
|
|
||||||
pr.files.push_back(pr_file);
|
pr.mergeable = extract_bool_field(response, "mergeable", false);
|
||||||
}
|
pr.mergeable_state = extract_string_field(response, "mergeable_state", "unknown");
|
||||||
}
|
} else if (platform == GitPlatform::GitLab) {
|
||||||
}
|
pr.base_ref = extract_string_field(response, "target_branch");
|
||||||
}
|
pr.head_ref = extract_string_field(response, "source_branch");
|
||||||
|
|
||||||
return pr;
|
if (auto diff_refs = extract_object_segment(response, "diff_refs")) {
|
||||||
}
|
pr.base_sha = extract_string_field(*diff_refs, "base_sha");
|
||||||
|
pr.head_sha = extract_string_field(*diff_refs, "head_sha");
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<std::vector<std::string>>
|
std::string merge_status = extract_string_field(response, "merge_status");
|
||||||
fetch_file_content(GitPlatform platform, const std::string &owner,
|
pr.mergeable = (merge_status == "can_be_merged");
|
||||||
const std::string &repo, const std::string &sha,
|
pr.mergeable_state = merge_status;
|
||||||
const std::string &path, const std::string &token) {
|
|
||||||
std::string url;
|
|
||||||
|
|
||||||
if (platform == GitPlatform::GitHub) {
|
|
||||||
// GitHub API URL
|
|
||||||
url = "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" +
|
|
||||||
path + "?ref=" + sha;
|
|
||||||
} else if (platform == GitPlatform::GitLab) {
|
|
||||||
// GitLab API URL - encode project path and file path
|
|
||||||
std::string project_path = owner;
|
|
||||||
if (!repo.empty()) {
|
|
||||||
project_path += "/" + repo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CURL *curl = curl_easy_init();
|
// Fetch PR/MR files
|
||||||
char *encoded_project =
|
std::string files_response;
|
||||||
curl_easy_escape(curl, project_path.c_str(), project_path.length());
|
|
||||||
char *encoded_path = curl_easy_escape(curl, path.c_str(), path.length());
|
|
||||||
|
|
||||||
url = "https://gitlab.com/api/v4/projects/" + std::string(encoded_project) +
|
if (!http_get(files_url, token, files_response, platform)) {
|
||||||
"/repository/files/" + std::string(encoded_path) + "/raw?ref=" + sha;
|
std::cerr << "Failed to fetch pull/merge request files" << std::endl;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process files based on platform
|
||||||
|
if (platform == GitPlatform::GitHub) {
|
||||||
|
for (const auto& file_object : extract_objects_from_array(files_response)) {
|
||||||
|
PRFile pr_file;
|
||||||
|
pr_file.filename = extract_string_field(file_object, "filename");
|
||||||
|
pr_file.status = extract_string_field(file_object, "status");
|
||||||
|
pr_file.additions = extract_int_field(file_object, "additions");
|
||||||
|
pr_file.deletions = extract_int_field(file_object, "deletions");
|
||||||
|
pr_file.changes = extract_int_field(file_object, "changes");
|
||||||
|
|
||||||
|
pr.files.push_back(pr_file);
|
||||||
|
}
|
||||||
|
} else if (platform == GitPlatform::GitLab) {
|
||||||
|
if (auto changes_array = extract_array_segment(files_response, "changes")) {
|
||||||
|
for (const auto& file_object : extract_objects_from_array(*changes_array)) {
|
||||||
|
PRFile pr_file;
|
||||||
|
std::string new_path = extract_string_field(file_object, "new_path");
|
||||||
|
if (!new_path.empty()) {
|
||||||
|
pr_file.filename = new_path;
|
||||||
|
} else {
|
||||||
|
pr_file.filename = extract_string_field(file_object, "old_path");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool new_file = extract_bool_field(file_object, "new_file", false);
|
||||||
|
bool deleted_file = extract_bool_field(file_object, "deleted_file", false);
|
||||||
|
bool renamed_file = extract_bool_field(file_object, "renamed_file", false);
|
||||||
|
|
||||||
|
if (new_file) {
|
||||||
|
pr_file.status = "added";
|
||||||
|
} else if (deleted_file) {
|
||||||
|
pr_file.status = "removed";
|
||||||
|
} else if (renamed_file) {
|
||||||
|
pr_file.status = "renamed";
|
||||||
|
} else {
|
||||||
|
pr_file.status = "modified";
|
||||||
|
}
|
||||||
|
|
||||||
|
pr_file.additions = 0;
|
||||||
|
pr_file.deletions = 0;
|
||||||
|
pr_file.changes = 0;
|
||||||
|
|
||||||
|
pr.files.push_back(pr_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::vector<std::string>> fetch_file_content(
|
||||||
|
GitPlatform platform,
|
||||||
|
const std::string& owner,
|
||||||
|
const std::string& repo,
|
||||||
|
const std::string& sha,
|
||||||
|
const std::string& path,
|
||||||
|
const std::string& token
|
||||||
|
) {
|
||||||
|
std::string url;
|
||||||
|
|
||||||
|
if (platform == GitPlatform::GitHub) {
|
||||||
|
// GitHub API URL
|
||||||
|
url = "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" + path + "?ref=" + sha;
|
||||||
|
} else if (platform == GitPlatform::GitLab) {
|
||||||
|
// GitLab API URL - encode project path and file path
|
||||||
|
std::string project_path = owner;
|
||||||
|
if (!repo.empty()) {
|
||||||
|
project_path += "/" + repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
CURL* curl = curl_easy_init();
|
||||||
|
char* encoded_project = curl_easy_escape(curl, project_path.c_str(), project_path.length());
|
||||||
|
char* encoded_path = curl_easy_escape(curl, path.c_str(), path.length());
|
||||||
|
|
||||||
|
url = "https://gitlab.com/api/v4/projects/" + std::string(encoded_project) +
|
||||||
|
"/repository/files/" + std::string(encoded_path) + "/raw?ref=" + sha;
|
||||||
|
|
||||||
|
curl_free(encoded_project);
|
||||||
|
curl_free(encoded_path);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
} else {
|
||||||
|
std::cerr << "Unknown platform" << std::endl;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string response;
|
||||||
|
|
||||||
|
if (!http_get(url, token, response, platform)) {
|
||||||
|
std::cerr << "Failed to fetch file content for " << path << " at " << sha << std::endl;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle response based on platform
|
||||||
|
if (platform == GitPlatform::GitHub) {
|
||||||
|
// GitHub returns JSON with base64-encoded content
|
||||||
|
std::string encoding = extract_string_field(response, "encoding");
|
||||||
|
std::string encoded_content = extract_string_field(response, "content");
|
||||||
|
|
||||||
|
if (encoding.empty() || encoded_content.empty()) {
|
||||||
|
std::cerr << "Invalid response format for file content" << std::endl;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding != "base64") {
|
||||||
|
std::cerr << "Unsupported encoding: " << encoding << std::endl;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove newlines from base64 string
|
||||||
|
encoded_content.erase(std::remove(encoded_content.begin(), encoded_content.end(), '\n'), encoded_content.end());
|
||||||
|
encoded_content.erase(std::remove(encoded_content.begin(), encoded_content.end(), '\r'), encoded_content.end());
|
||||||
|
|
||||||
|
// Decode base64
|
||||||
|
std::string decoded_content = base64_decode(encoded_content);
|
||||||
|
|
||||||
|
if (decoded_content.empty()) {
|
||||||
|
std::cerr << "Failed to decode base64 content" << std::endl;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split content into lines
|
||||||
|
return split_lines(decoded_content);
|
||||||
|
} else if (platform == GitPlatform::GitLab) {
|
||||||
|
// GitLab returns raw file content directly
|
||||||
|
return split_lines(response);
|
||||||
|
}
|
||||||
|
|
||||||
curl_free(encoded_project);
|
|
||||||
curl_free(encoded_path);
|
|
||||||
curl_easy_cleanup(curl);
|
|
||||||
} else {
|
|
||||||
std::cerr << "Unknown platform" << std::endl;
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
|
||||||
|
|
||||||
std::string response;
|
|
||||||
|
|
||||||
if (!http_get(url, token, response, platform)) {
|
|
||||||
std::cerr << "Failed to fetch file content for " << path << " at " << sha
|
|
||||||
<< std::endl;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle response based on platform
|
|
||||||
if (platform == GitPlatform::GitHub) {
|
|
||||||
// GitHub returns JSON with base64-encoded content
|
|
||||||
Json::Value root;
|
|
||||||
Json::CharReaderBuilder reader;
|
|
||||||
std::string errs;
|
|
||||||
std::istringstream s(response);
|
|
||||||
|
|
||||||
if (!Json::parseFromStream(reader, s, &root, &errs)) {
|
|
||||||
std::cerr << "Failed to parse content JSON: " << errs << std::endl;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GitHub API returns content as base64 encoded
|
|
||||||
if (!root.isMember("content") || !root.isMember("encoding")) {
|
|
||||||
std::cerr << "Invalid response format for file content" << std::endl;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string encoding = root["encoding"].asString();
|
|
||||||
if (encoding != "base64") {
|
|
||||||
std::cerr << "Unsupported encoding: " << encoding << std::endl;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode base64 content
|
|
||||||
std::string encoded_content = root["content"].asString();
|
|
||||||
|
|
||||||
// Remove newlines from base64 string
|
|
||||||
encoded_content.erase(
|
|
||||||
std::remove(encoded_content.begin(), encoded_content.end(), '\n'),
|
|
||||||
encoded_content.end());
|
|
||||||
encoded_content.erase(
|
|
||||||
std::remove(encoded_content.begin(), encoded_content.end(), '\r'),
|
|
||||||
encoded_content.end());
|
|
||||||
|
|
||||||
// Decode base64
|
|
||||||
std::string decoded_content = base64_decode(encoded_content);
|
|
||||||
|
|
||||||
if (decoded_content.empty()) {
|
|
||||||
std::cerr << "Failed to decode base64 content" << std::endl;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split content into lines
|
|
||||||
return split_lines(decoded_content);
|
|
||||||
} else if (platform == GitPlatform::GitLab) {
|
|
||||||
// GitLab returns raw file content directly
|
|
||||||
return split_lines(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace git
|
} // namespace git
|
||||||
} // namespace wizardmerge
|
} // namespace wizardmerge
|
||||||
|
|||||||
@@ -3,52 +3,52 @@
|
|||||||
* @brief HTTP API server for WizardMerge using Drogon framework
|
* @brief HTTP API server for WizardMerge using Drogon framework
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "controllers/MergeController.h"
|
|
||||||
#include <drogon/drogon.h>
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <drogon/drogon.h>
|
||||||
|
#include "controllers/MergeController.h"
|
||||||
|
|
||||||
using namespace drogon;
|
using namespace drogon;
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
std::cout << "WizardMerge - Intelligent Merge Conflict Resolution API\n";
|
std::cout << "WizardMerge - Intelligent Merge Conflict Resolution API\n";
|
||||||
std::cout << "======================================================\n";
|
std::cout << "======================================================\n";
|
||||||
std::cout << "Starting HTTP server...\n\n";
|
std::cout << "Starting HTTP server...\n\n";
|
||||||
|
|
||||||
// Load configuration from file
|
// Load configuration from file
|
||||||
std::string config_file = "config.json";
|
std::string config_file = "config.json";
|
||||||
if (argc > 1) {
|
if (argc > 1) {
|
||||||
config_file = argv[1];
|
config_file = argv[1];
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Load configuration and start server
|
|
||||||
app().loadConfigFile(config_file);
|
|
||||||
|
|
||||||
// Display listener information if available
|
|
||||||
auto listeners = app().getListeners();
|
|
||||||
if (!listeners.empty()) {
|
|
||||||
try {
|
|
||||||
std::cout << "Server will listen on port " << listeners[0].toPort
|
|
||||||
<< "\n";
|
|
||||||
} catch (...) {
|
|
||||||
std::cout << "Server listener configured\n";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
std::cout << "Server configuration loaded\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "Available endpoints:\n";
|
try {
|
||||||
std::cout << " POST /api/merge - Three-way merge API\n";
|
// Load configuration and start server
|
||||||
std::cout << "\nPress Ctrl+C to stop the server.\n\n";
|
app().loadConfigFile(config_file);
|
||||||
|
|
||||||
// Run the application
|
// Display listener information if available
|
||||||
app().run();
|
auto listeners = app().getListeners();
|
||||||
} catch (const std::exception &e) {
|
if (!listeners.empty()) {
|
||||||
std::cerr << "Error: " << e.what() << '\n';
|
try {
|
||||||
std::cerr << "Failed to load config file: " << config_file << '\n';
|
std::cout << "Server will listen on port "
|
||||||
std::cerr << "Usage: " << argv[0] << " [config.json]\n";
|
<< listeners[0].toPort << "\n";
|
||||||
return 1;
|
} catch (...) {
|
||||||
}
|
std::cout << "Server listener configured\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::cout << "Server configuration loaded\n";
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
std::cout << "Available endpoints:\n";
|
||||||
|
std::cout << " POST /api/merge - Three-way merge API\n";
|
||||||
|
std::cout << "\nPress Ctrl+C to stop the server.\n\n";
|
||||||
|
|
||||||
|
// Run the application
|
||||||
|
app().run();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Error: " << e.what() << '\n';
|
||||||
|
std::cerr << "Failed to load config file: " << config_file << '\n';
|
||||||
|
std::cerr << "Usage: " << argv[0] << " [config.json]\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,118 +16,117 @@ namespace {
|
|||||||
/**
|
/**
|
||||||
* @brief Check if two lines are effectively equal (ignoring whitespace).
|
* @brief Check if two lines are effectively equal (ignoring whitespace).
|
||||||
*/
|
*/
|
||||||
bool lines_equal_ignore_whitespace(const std::string &a, const std::string &b) {
|
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)
|
if (start == std::string::npos) return std::string();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
MergeResult three_way_merge(const std::vector<std::string> &base,
|
MergeResult three_way_merge(
|
||||||
const std::vector<std::string> &ours,
|
const std::vector<std::string>& base,
|
||||||
const std::vector<std::string> &theirs) {
|
const std::vector<std::string>& ours,
|
||||||
MergeResult result;
|
const std::vector<std::string>& theirs
|
||||||
|
) {
|
||||||
|
MergeResult result;
|
||||||
|
|
||||||
// Simple line-by-line comparison for initial implementation
|
// Simple line-by-line comparison for initial implementation
|
||||||
// This is a placeholder - full algorithm will use dependency analysis
|
// This is a placeholder - full algorithm will use dependency analysis
|
||||||
|
|
||||||
size_t max_len = std::max({base.size(), ours.size(), theirs.size()});
|
size_t max_len = std::max({base.size(), ours.size(), theirs.size()});
|
||||||
|
|
||||||
for (size_t i = 0; i < max_len; ++i) {
|
for (size_t i = 0; i < max_len; ++i) {
|
||||||
std::string base_line = (i < base.size()) ? base[i] : "";
|
std::string base_line = (i < base.size()) ? base[i] : "";
|
||||||
std::string our_line = (i < ours.size()) ? ours[i] : "";
|
std::string our_line = (i < ours.size()) ? ours[i] : "";
|
||||||
std::string their_line = (i < theirs.size()) ? theirs[i] : "";
|
std::string their_line = (i < theirs.size()) ? theirs[i] : "";
|
||||||
|
|
||||||
// Case 1: All three are the same - use as-is
|
// Case 1: All three are the same - use as-is
|
||||||
if (base_line == our_line && base_line == their_line) {
|
if (base_line == our_line && base_line == their_line) {
|
||||||
result.merged_lines.push_back({base_line, Line::BASE});
|
result.merged_lines.push_back({base_line, Line::BASE});
|
||||||
}
|
}
|
||||||
// Case 2: Base == Ours, but Theirs changed - use theirs
|
// Case 2: Base == Ours, but Theirs changed - use theirs
|
||||||
else if (base_line == our_line && base_line != their_line) {
|
else if (base_line == our_line && base_line != their_line) {
|
||||||
result.merged_lines.push_back({their_line, Line::THEIRS});
|
result.merged_lines.push_back({their_line, Line::THEIRS});
|
||||||
}
|
}
|
||||||
// Case 3: Base == Theirs, but Ours changed - use ours
|
// Case 3: Base == Theirs, but Ours changed - use ours
|
||||||
else if (base_line == their_line && base_line != our_line) {
|
else if (base_line == their_line && base_line != our_line) {
|
||||||
result.merged_lines.push_back({our_line, Line::OURS});
|
result.merged_lines.push_back({our_line, Line::OURS});
|
||||||
}
|
}
|
||||||
// Case 4: Ours == Theirs, but different from Base - use the common change
|
// Case 4: Ours == Theirs, but different from Base - use the common change
|
||||||
else if (our_line == their_line && our_line != base_line) {
|
else if (our_line == their_line && our_line != base_line) {
|
||||||
result.merged_lines.push_back({our_line, Line::MERGED});
|
result.merged_lines.push_back({our_line, Line::MERGED});
|
||||||
}
|
}
|
||||||
// Case 5: All different - conflict
|
// Case 5: All different - conflict
|
||||||
else {
|
else {
|
||||||
Conflict conflict;
|
Conflict conflict;
|
||||||
conflict.start_line = result.merged_lines.size();
|
conflict.start_line = result.merged_lines.size();
|
||||||
conflict.base_lines.push_back({base_line, Line::BASE});
|
conflict.base_lines.push_back({base_line, Line::BASE});
|
||||||
conflict.our_lines.push_back({our_line, Line::OURS});
|
conflict.our_lines.push_back({our_line, Line::OURS});
|
||||||
conflict.their_lines.push_back({their_line, Line::THEIRS});
|
conflict.their_lines.push_back({their_line, Line::THEIRS});
|
||||||
conflict.end_line = result.merged_lines.size();
|
conflict.end_line = result.merged_lines.size();
|
||||||
|
|
||||||
// Perform context analysis using ours version as context
|
// Perform context analysis using ours version as context
|
||||||
// (could also use base or theirs, but ours is typically most relevant)
|
// (could also use base or theirs, but ours is typically most relevant)
|
||||||
conflict.context = analysis::analyze_context(ours, i, i);
|
conflict.context = analysis::analyze_context(ours, i, i);
|
||||||
|
|
||||||
// Perform risk analysis for different resolution strategies
|
// Perform risk analysis for different resolution strategies
|
||||||
std::vector<std::string> base_vec = {base_line};
|
std::vector<std::string> base_vec = {base_line};
|
||||||
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 =
|
conflict.risk_ours = analysis::analyze_risk_ours(base_vec, ours_vec, theirs_vec);
|
||||||
analysis::analyze_risk_ours(base_vec, ours_vec, theirs_vec);
|
conflict.risk_theirs = analysis::analyze_risk_theirs(base_vec, ours_vec, theirs_vec);
|
||||||
conflict.risk_theirs =
|
conflict.risk_both = analysis::analyze_risk_both(base_vec, ours_vec, theirs_vec);
|
||||||
analysis::analyze_risk_theirs(base_vec, ours_vec, theirs_vec);
|
|
||||||
conflict.risk_both =
|
result.conflicts.push_back(conflict);
|
||||||
analysis::analyze_risk_both(base_vec, ours_vec, theirs_vec);
|
|
||||||
|
// Add conflict markers
|
||||||
result.conflicts.push_back(conflict);
|
result.merged_lines.push_back({"<<<<<<< OURS", Line::MERGED});
|
||||||
|
result.merged_lines.push_back({our_line, Line::OURS});
|
||||||
// Add conflict markers
|
result.merged_lines.push_back({"=======", Line::MERGED});
|
||||||
result.merged_lines.push_back({"<<<<<<< OURS", Line::MERGED});
|
result.merged_lines.push_back({their_line, Line::THEIRS});
|
||||||
result.merged_lines.push_back({our_line, Line::OURS});
|
result.merged_lines.push_back({">>>>>>> THEIRS", Line::MERGED});
|
||||||
result.merged_lines.push_back({"=======", Line::MERGED});
|
|
||||||
result.merged_lines.push_back({their_line, Line::THEIRS});
|
|
||||||
result.merged_lines.push_back({">>>>>>> THEIRS", Line::MERGED});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
MergeResult auto_resolve(const MergeResult &result) {
|
|
||||||
MergeResult resolved = result;
|
|
||||||
|
|
||||||
// Auto-resolve whitespace-only differences
|
|
||||||
std::vector<Conflict> remaining_conflicts;
|
|
||||||
|
|
||||||
for (const auto &conflict : result.conflicts) {
|
|
||||||
bool can_resolve = false;
|
|
||||||
|
|
||||||
// Check if differences are whitespace-only
|
|
||||||
if (conflict.our_lines.size() == conflict.their_lines.size()) {
|
|
||||||
can_resolve = true;
|
|
||||||
for (size_t i = 0; i < conflict.our_lines.size(); ++i) {
|
|
||||||
if (!lines_equal_ignore_whitespace(conflict.our_lines[i].content,
|
|
||||||
conflict.their_lines[i].content)) {
|
|
||||||
can_resolve = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!can_resolve) {
|
return result;
|
||||||
remaining_conflicts.push_back(conflict);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved.conflicts = remaining_conflicts;
|
|
||||||
return resolved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace merge
|
MergeResult auto_resolve(const MergeResult& result) {
|
||||||
} // namespace wizardmerge
|
MergeResult resolved = result;
|
||||||
|
|
||||||
|
// Auto-resolve whitespace-only differences
|
||||||
|
std::vector<Conflict> remaining_conflicts;
|
||||||
|
|
||||||
|
for (const auto& conflict : result.conflicts) {
|
||||||
|
bool can_resolve = false;
|
||||||
|
|
||||||
|
// Check if differences are whitespace-only
|
||||||
|
if (conflict.our_lines.size() == conflict.their_lines.size()) {
|
||||||
|
can_resolve = true;
|
||||||
|
for (size_t i = 0; i < conflict.our_lines.size(); ++i) {
|
||||||
|
if (!lines_equal_ignore_whitespace(
|
||||||
|
conflict.our_lines[i].content,
|
||||||
|
conflict.their_lines[i].content)) {
|
||||||
|
can_resolve = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!can_resolve) {
|
||||||
|
remaining_conflicts.push_back(conflict);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved.conflicts = remaining_conflicts;
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace merge
|
||||||
|
} // namespace wizardmerge
|
||||||
|
|||||||
@@ -12,167 +12,209 @@ using namespace wizardmerge::analysis;
|
|||||||
* Test basic context analysis
|
* Test basic context analysis
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, BasicContextAnalysis) {
|
TEST(ContextAnalyzerTest, BasicContextAnalysis) {
|
||||||
std::vector<std::string> lines = {"#include <iostream>",
|
std::vector<std::string> lines = {
|
||||||
"",
|
"#include <iostream>",
|
||||||
"class MyClass {",
|
"",
|
||||||
"public:",
|
"class MyClass {",
|
||||||
" void myMethod() {",
|
"public:",
|
||||||
" int x = 42;",
|
" void myMethod() {",
|
||||||
" int y = 100;",
|
" int x = 42;",
|
||||||
" return;",
|
" int y = 100;",
|
||||||
" }",
|
" return;",
|
||||||
"};"};
|
" }",
|
||||||
|
"};"
|
||||||
|
};
|
||||||
|
|
||||||
auto context = analyze_context(lines, 5, 7);
|
auto context = analyze_context(lines, 5, 7);
|
||||||
|
|
||||||
EXPECT_EQ(context.start_line, 5);
|
EXPECT_EQ(context.start_line, 5);
|
||||||
EXPECT_EQ(context.end_line, 7);
|
EXPECT_EQ(context.end_line, 7);
|
||||||
EXPECT_FALSE(context.surrounding_lines.empty());
|
EXPECT_FALSE(context.surrounding_lines.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test function name extraction
|
* Test function name extraction
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, ExtractFunctionName) {
|
TEST(ContextAnalyzerTest, ExtractFunctionName) {
|
||||||
std::vector<std::string> lines = {"void testFunction() {", " int x = 10;",
|
std::vector<std::string> lines = {
|
||||||
" return;", "}"};
|
"void testFunction() {",
|
||||||
|
" 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test Python function name extraction
|
* Test Python function name extraction
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, ExtractPythonFunctionName) {
|
TEST(ContextAnalyzerTest, ExtractPythonFunctionName) {
|
||||||
std::vector<std::string> lines = {"def my_python_function():", " x = 10",
|
std::vector<std::string> lines = {
|
||||||
" return x"};
|
"def my_python_function():",
|
||||||
|
" 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test class name extraction
|
* Test class name extraction
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, ExtractClassName) {
|
TEST(ContextAnalyzerTest, ExtractClassName) {
|
||||||
std::vector<std::string> lines = {"class TestClass {", " int member;",
|
std::vector<std::string> lines = {
|
||||||
"};"};
|
"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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test import extraction
|
* Test import extraction
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, ExtractImports) {
|
TEST(ContextAnalyzerTest, ExtractImports) {
|
||||||
std::vector<std::string> lines = {
|
std::vector<std::string> lines = {
|
||||||
"#include <iostream>", "#include <vector>", "",
|
"#include <iostream>",
|
||||||
"int main() {", " return 0;", "}"};
|
"#include <vector>",
|
||||||
|
"",
|
||||||
|
"int main() {",
|
||||||
|
" return 0;",
|
||||||
|
"}"
|
||||||
|
};
|
||||||
|
|
||||||
auto imports = extract_imports(lines);
|
auto imports = extract_imports(lines);
|
||||||
EXPECT_EQ(imports.size(), 2);
|
EXPECT_EQ(imports.size(), 2);
|
||||||
EXPECT_EQ(imports[0], "#include <iostream>");
|
EXPECT_EQ(imports[0], "#include <iostream>");
|
||||||
EXPECT_EQ(imports[1], "#include <vector>");
|
EXPECT_EQ(imports[1], "#include <vector>");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test context with no function
|
* Test context with no function
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, NoFunctionContext) {
|
TEST(ContextAnalyzerTest, NoFunctionContext) {
|
||||||
std::vector<std::string> lines = {"int x = 10;", "int y = 20;"};
|
std::vector<std::string> lines = {
|
||||||
|
"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, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test context window boundaries
|
* Test context window boundaries
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, ContextWindowBoundaries) {
|
TEST(ContextAnalyzerTest, ContextWindowBoundaries) {
|
||||||
std::vector<std::string> lines = {"line1", "line2", "line3", "line4",
|
std::vector<std::string> lines = {
|
||||||
"line5"};
|
"line1",
|
||||||
|
"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);
|
||||||
EXPECT_GE(context.surrounding_lines.size(), 1);
|
EXPECT_GE(context.surrounding_lines.size(), 1);
|
||||||
|
|
||||||
// Test with context window at end of file
|
// Test with context window at end of file
|
||||||
context = analyze_context(lines, 4, 4, 2);
|
context = analyze_context(lines, 4, 4, 2);
|
||||||
EXPECT_GE(context.surrounding_lines.size(), 1);
|
EXPECT_GE(context.surrounding_lines.size(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test TypeScript function detection
|
* Test TypeScript function detection
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, TypeScriptFunctionDetection) {
|
TEST(ContextAnalyzerTest, TypeScriptFunctionDetection) {
|
||||||
std::vector<std::string> lines = {"export async function fetchData() {",
|
std::vector<std::string> lines = {
|
||||||
" const data = await api.get();",
|
"export async function fetchData() {",
|
||||||
" return data;", "}"};
|
" const data = await api.get();",
|
||||||
|
" 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test TypeScript arrow function detection
|
* Test TypeScript arrow function detection
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, TypeScriptArrowFunctionDetection) {
|
TEST(ContextAnalyzerTest, TypeScriptArrowFunctionDetection) {
|
||||||
std::vector<std::string> lines = {
|
std::vector<std::string> lines = {
|
||||||
"const handleClick = (event: MouseEvent) => {", " console.log(event);",
|
"const handleClick = (event: MouseEvent) => {",
|
||||||
"};"};
|
" 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test TypeScript interface detection
|
* Test TypeScript interface detection
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, TypeScriptInterfaceDetection) {
|
TEST(ContextAnalyzerTest, TypeScriptInterfaceDetection) {
|
||||||
std::vector<std::string> lines = {
|
std::vector<std::string> lines = {
|
||||||
"export interface User {", " id: number;", " name: string;", "}"};
|
"export interface User {",
|
||||||
|
" 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test TypeScript type alias detection
|
* Test TypeScript type alias detection
|
||||||
*/
|
*/
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test TypeScript enum detection
|
* Test TypeScript enum detection
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, TypeScriptEnumDetection) {
|
TEST(ContextAnalyzerTest, TypeScriptEnumDetection) {
|
||||||
std::vector<std::string> lines = {"enum Color {", " Red,", " Green,",
|
std::vector<std::string> lines = {
|
||||||
" Blue", "}"};
|
"enum Color {",
|
||||||
|
" 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test TypeScript import extraction
|
* Test TypeScript import extraction
|
||||||
*/
|
*/
|
||||||
TEST(ContextAnalyzerTest, TypeScriptImportExtraction) {
|
TEST(ContextAnalyzerTest, TypeScriptImportExtraction) {
|
||||||
std::vector<std::string> lines = {"import { Component } from 'react';",
|
std::vector<std::string> lines = {
|
||||||
"import type { User } from './types';",
|
"import { Component } from 'react';",
|
||||||
"import * as utils from './utils';",
|
"import type { User } from './types';",
|
||||||
"",
|
"import * as utils from './utils';",
|
||||||
"function MyComponent() {",
|
"",
|
||||||
" return null;",
|
"function MyComponent() {",
|
||||||
"}"};
|
" return null;",
|
||||||
|
"}"
|
||||||
|
};
|
||||||
|
|
||||||
auto imports = extract_imports(lines);
|
auto imports = extract_imports(lines);
|
||||||
EXPECT_GE(imports.size(), 3);
|
EXPECT_GE(imports.size(), 3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,220 +4,203 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#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;
|
||||||
|
|
||||||
class GitCLITest : public ::testing::Test {
|
class GitCLITest : public ::testing::Test {
|
||||||
protected:
|
protected:
|
||||||
std::string test_dir;
|
std::string test_dir;
|
||||||
|
|
||||||
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 =
|
test_dir = (temp_base / ("wizardmerge_git_test_" + std::to_string(time(nullptr)))).string();
|
||||||
(temp_base / ("wizardmerge_git_test_" + std::to_string(time(nullptr))))
|
fs::create_directories(test_dir);
|
||||||
.string();
|
|
||||||
fs::create_directories(test_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
// Clean up test directory
|
|
||||||
if (fs::exists(test_dir)) {
|
|
||||||
fs::remove_all(test_dir);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to initialize a git repo
|
void TearDown() override {
|
||||||
void init_repo(const std::string &path) {
|
// Clean up test directory
|
||||||
system(("git init \"" + path + "\" 2>&1 > /dev/null").c_str());
|
if (fs::exists(test_dir)) {
|
||||||
system(("git -C \"" + path + "\" config user.name \"Test User\"").c_str());
|
fs::remove_all(test_dir);
|
||||||
system(("git -C \"" + path + "\" config user.email \"test@example.com\"")
|
}
|
||||||
.c_str());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to create a file
|
// Helper to initialize a git repo
|
||||||
void create_file(const std::string &path, const std::string &content) {
|
void init_repo(const std::string& path) {
|
||||||
std::ofstream file(path);
|
system(("git init \"" + path + "\" 2>&1 > /dev/null").c_str());
|
||||||
file << content;
|
system(("git -C \"" + path + "\" config user.name \"Test User\"").c_str());
|
||||||
file.close();
|
system(("git -C \"" + path + "\" config user.email \"test@example.com\"").c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to create a file
|
||||||
|
void create_file(const std::string& path, const std::string& content) {
|
||||||
|
std::ofstream file(path);
|
||||||
|
file << content;
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test Git availability check
|
* Test Git availability check
|
||||||
*/
|
*/
|
||||||
TEST_F(GitCLITest, GitAvailability) {
|
TEST_F(GitCLITest, GitAvailability) {
|
||||||
// Git should be available in CI environment
|
// Git should be available in CI environment
|
||||||
EXPECT_TRUE(is_git_available());
|
EXPECT_TRUE(is_git_available());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test branch existence check
|
* Test branch existence check
|
||||||
*/
|
*/
|
||||||
TEST_F(GitCLITest, BranchExists) {
|
TEST_F(GitCLITest, BranchExists) {
|
||||||
std::string repo_path = test_dir + "/test_repo";
|
std::string repo_path = test_dir + "/test_repo";
|
||||||
init_repo(repo_path);
|
init_repo(repo_path);
|
||||||
|
|
||||||
// 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(
|
system(("git -C \"" + repo_path + "\" add test.txt 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());
|
||||||
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);
|
||||||
ASSERT_TRUE(current_branch.has_value());
|
ASSERT_TRUE(current_branch.has_value());
|
||||||
EXPECT_TRUE(branch_exists(repo_path, current_branch.value()));
|
EXPECT_TRUE(branch_exists(repo_path, current_branch.value()));
|
||||||
|
|
||||||
// Non-existent branch should not exist
|
// Non-existent branch should not exist
|
||||||
EXPECT_FALSE(branch_exists(repo_path, "nonexistent-branch"));
|
EXPECT_FALSE(branch_exists(repo_path, "nonexistent-branch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test getting current branch
|
* Test getting current branch
|
||||||
*/
|
*/
|
||||||
TEST_F(GitCLITest, GetCurrentBranch) {
|
TEST_F(GitCLITest, GetCurrentBranch) {
|
||||||
std::string repo_path = test_dir + "/test_repo";
|
std::string repo_path = test_dir + "/test_repo";
|
||||||
init_repo(repo_path);
|
init_repo(repo_path);
|
||||||
|
|
||||||
// Create initial commit
|
// Create initial commit
|
||||||
create_file(repo_path + "/test.txt", "initial content");
|
create_file(repo_path + "/test.txt", "initial content");
|
||||||
system(
|
system(("git -C \"" + repo_path + "\" add test.txt 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());
|
||||||
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());
|
||||||
// Should be either "main" or "master" depending on Git version
|
// Should be either "main" or "master" depending on Git version
|
||||||
EXPECT_TRUE(branch.value() == "main" || branch.value() == "master");
|
EXPECT_TRUE(branch.value() == "main" || branch.value() == "master");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test creating a new branch
|
* Test creating a new branch
|
||||||
*/
|
*/
|
||||||
TEST_F(GitCLITest, CreateBranch) {
|
TEST_F(GitCLITest, CreateBranch) {
|
||||||
std::string repo_path = test_dir + "/test_repo";
|
std::string repo_path = test_dir + "/test_repo";
|
||||||
init_repo(repo_path);
|
init_repo(repo_path);
|
||||||
|
|
||||||
// Create initial commit
|
// Create initial commit
|
||||||
create_file(repo_path + "/test.txt", "initial content");
|
create_file(repo_path + "/test.txt", "initial content");
|
||||||
system(
|
system(("git -C \"" + repo_path + "\" add test.txt 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());
|
||||||
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");
|
||||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||||
|
|
||||||
// Verify we're on the new branch
|
// Verify we're on the new branch
|
||||||
auto current_branch = get_current_branch(repo_path);
|
auto current_branch = get_current_branch(repo_path);
|
||||||
ASSERT_TRUE(current_branch.has_value());
|
ASSERT_TRUE(current_branch.has_value());
|
||||||
EXPECT_EQ(current_branch.value(), "test-branch");
|
EXPECT_EQ(current_branch.value(), "test-branch");
|
||||||
|
|
||||||
// Verify branch exists
|
// Verify branch exists
|
||||||
EXPECT_TRUE(branch_exists(repo_path, "test-branch"));
|
EXPECT_TRUE(branch_exists(repo_path, "test-branch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test adding files
|
* Test adding files
|
||||||
*/
|
*/
|
||||||
TEST_F(GitCLITest, AddFiles) {
|
TEST_F(GitCLITest, AddFiles) {
|
||||||
std::string repo_path = test_dir + "/test_repo";
|
std::string repo_path = test_dir + "/test_repo";
|
||||||
init_repo(repo_path);
|
init_repo(repo_path);
|
||||||
|
|
||||||
// Create test files
|
// Create test files
|
||||||
create_file(repo_path + "/file1.txt", "content1");
|
create_file(repo_path + "/file1.txt", "content1");
|
||||||
create_file(repo_path + "/file2.txt", "content2");
|
create_file(repo_path + "/file2.txt", "content2");
|
||||||
|
|
||||||
// Add files
|
// Add files
|
||||||
GitResult result = add_files(repo_path, {"file1.txt", "file2.txt"});
|
GitResult result = add_files(repo_path, {"file1.txt", "file2.txt"});
|
||||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test committing changes
|
* Test committing changes
|
||||||
*/
|
*/
|
||||||
TEST_F(GitCLITest, Commit) {
|
TEST_F(GitCLITest, Commit) {
|
||||||
std::string repo_path = test_dir + "/test_repo";
|
std::string repo_path = test_dir + "/test_repo";
|
||||||
init_repo(repo_path);
|
init_repo(repo_path);
|
||||||
|
|
||||||
// Create and add a file
|
// Create and add a file
|
||||||
create_file(repo_path + "/test.txt", "content");
|
create_file(repo_path + "/test.txt", "content");
|
||||||
add_files(repo_path, {"test.txt"});
|
add_files(repo_path, {"test.txt"});
|
||||||
|
|
||||||
// Commit
|
// Commit
|
||||||
GitConfig config;
|
GitConfig config;
|
||||||
config.user_name = "Test User";
|
config.user_name = "Test User";
|
||||||
config.user_email = "test@example.com";
|
config.user_email = "test@example.com";
|
||||||
|
|
||||||
GitResult result = commit(repo_path, "Test commit", config);
|
GitResult result = commit(repo_path, "Test commit", config);
|
||||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test repository status
|
* Test repository status
|
||||||
*/
|
*/
|
||||||
TEST_F(GitCLITest, Status) {
|
TEST_F(GitCLITest, Status) {
|
||||||
std::string repo_path = test_dir + "/test_repo";
|
std::string repo_path = test_dir + "/test_repo";
|
||||||
init_repo(repo_path);
|
init_repo(repo_path);
|
||||||
|
|
||||||
GitResult result = status(repo_path);
|
GitResult result = status(repo_path);
|
||||||
EXPECT_TRUE(result.success);
|
EXPECT_TRUE(result.success);
|
||||||
EXPECT_FALSE(result.output.empty());
|
EXPECT_FALSE(result.output.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test checkout branch
|
* Test checkout branch
|
||||||
*/
|
*/
|
||||||
TEST_F(GitCLITest, CheckoutBranch) {
|
TEST_F(GitCLITest, CheckoutBranch) {
|
||||||
std::string repo_path = test_dir + "/test_repo";
|
std::string repo_path = test_dir + "/test_repo";
|
||||||
init_repo(repo_path);
|
init_repo(repo_path);
|
||||||
|
|
||||||
// Create initial commit
|
// Create initial commit
|
||||||
create_file(repo_path + "/test.txt", "initial content");
|
create_file(repo_path + "/test.txt", "initial content");
|
||||||
system(
|
system(("git -C \"" + repo_path + "\" add test.txt 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());
|
||||||
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() +
|
system(("git -C \"" + repo_path + "\" checkout " + original_branch.value() + " 2>&1 > /dev/null").c_str());
|
||||||
" 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");
|
||||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||||
|
|
||||||
// Verify we're on test-branch
|
// Verify we're on test-branch
|
||||||
auto current_branch = get_current_branch(repo_path);
|
auto current_branch = get_current_branch(repo_path);
|
||||||
ASSERT_TRUE(current_branch.has_value());
|
ASSERT_TRUE(current_branch.has_value());
|
||||||
EXPECT_EQ(current_branch.value(), "test-branch");
|
EXPECT_EQ(current_branch.value(), "test-branch");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test empty file list
|
* Test empty file list
|
||||||
*/
|
*/
|
||||||
TEST_F(GitCLITest, AddEmptyFileList) {
|
TEST_F(GitCLITest, AddEmptyFileList) {
|
||||||
std::string repo_path = test_dir + "/test_repo";
|
std::string repo_path = test_dir + "/test_repo";
|
||||||
init_repo(repo_path);
|
init_repo(repo_path);
|
||||||
|
|
||||||
// Add empty file list should succeed without error
|
// Add empty file list should succeed without error
|
||||||
GitResult result = add_files(repo_path, {});
|
GitResult result = add_files(repo_path, {});
|
||||||
EXPECT_TRUE(result.success);
|
EXPECT_TRUE(result.success);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,121 +12,105 @@ using namespace wizardmerge::git;
|
|||||||
* Test PR URL parsing with various GitHub formats
|
* Test PR URL parsing with various GitHub formats
|
||||||
*/
|
*/
|
||||||
TEST(GitPlatformClientTest, ParseGitHubPRUrl_ValidUrls) {
|
TEST(GitPlatformClientTest, ParseGitHubPRUrl_ValidUrls) {
|
||||||
GitPlatform platform;
|
GitPlatform platform;
|
||||||
std::string owner, repo;
|
std::string owner, repo;
|
||||||
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,
|
ASSERT_TRUE(parse_pr_url("https://github.com/owner/repo/pull/123", platform, owner, repo, pr_number));
|
||||||
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,
|
ASSERT_TRUE(parse_pr_url("github.com/user/project/pull/456", platform, owner, repo, pr_number));
|
||||||
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",
|
ASSERT_TRUE(parse_pr_url("https://www.github.com/testuser/testrepo/pull/789", platform, owner, repo, pr_number));
|
||||||
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");
|
EXPECT_EQ(pr_number, 789);
|
||||||
EXPECT_EQ(pr_number, 789);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test GitLab MR URL parsing with various formats
|
* Test GitLab MR URL parsing with various formats
|
||||||
*/
|
*/
|
||||||
TEST(GitPlatformClientTest, ParseGitLabMRUrl_ValidUrls) {
|
TEST(GitPlatformClientTest, ParseGitLabMRUrl_ValidUrls) {
|
||||||
GitPlatform platform;
|
GitPlatform platform;
|
||||||
std::string owner, repo;
|
std::string owner, repo;
|
||||||
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",
|
ASSERT_TRUE(parse_pr_url("https://gitlab.com/owner/repo/-/merge_requests/123", platform, owner, repo, pr_number));
|
||||||
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(
|
ASSERT_TRUE(parse_pr_url("https://gitlab.com/group/subgroup/project/-/merge_requests/456", platform, owner, repo, pr_number));
|
||||||
"https://gitlab.com/group/subgroup/project/-/merge_requests/456",
|
EXPECT_EQ(platform, GitPlatform::GitLab);
|
||||||
platform, owner, repo, pr_number));
|
EXPECT_EQ(owner, "group/subgroup");
|
||||||
EXPECT_EQ(platform, GitPlatform::GitLab);
|
EXPECT_EQ(repo, "project");
|
||||||
EXPECT_EQ(owner, "group/subgroup");
|
EXPECT_EQ(pr_number, 456);
|
||||||
EXPECT_EQ(repo, "project");
|
|
||||||
EXPECT_EQ(pr_number, 456);
|
|
||||||
|
|
||||||
// Test without https://
|
// Test without https://
|
||||||
ASSERT_TRUE(parse_pr_url("gitlab.com/mygroup/myproject/-/merge_requests/789",
|
ASSERT_TRUE(parse_pr_url("gitlab.com/mygroup/myproject/-/merge_requests/789", platform, owner, repo, pr_number));
|
||||||
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");
|
EXPECT_EQ(pr_number, 789);
|
||||||
EXPECT_EQ(pr_number, 789);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test PR/MR URL parsing with invalid formats
|
* Test PR/MR URL parsing with invalid formats
|
||||||
*/
|
*/
|
||||||
TEST(GitPlatformClientTest, ParsePRUrl_InvalidUrls) {
|
TEST(GitPlatformClientTest, ParsePRUrl_InvalidUrls) {
|
||||||
GitPlatform platform;
|
GitPlatform platform;
|
||||||
std::string owner, repo;
|
std::string owner, repo;
|
||||||
int pr_number;
|
int pr_number;
|
||||||
|
|
||||||
// Missing PR number
|
// Missing PR number
|
||||||
EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo/pull/", platform,
|
EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo/pull/", platform, owner, repo, pr_number));
|
||||||
owner, repo, pr_number));
|
|
||||||
|
|
||||||
// Invalid format
|
// Invalid format
|
||||||
EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo", platform, owner,
|
EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo", platform, owner, repo, pr_number));
|
||||||
repo, pr_number));
|
|
||||||
|
|
||||||
// Not a GitHub or GitLab URL
|
// Not a GitHub or GitLab URL
|
||||||
EXPECT_FALSE(
|
EXPECT_FALSE(parse_pr_url("https://bitbucket.org/owner/repo/pull-requests/123", platform, owner, repo, pr_number));
|
||||||
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,
|
EXPECT_FALSE(parse_pr_url("https://gitlab.com/owner/repo/pull/123", platform, owner, repo, pr_number));
|
||||||
owner, repo, pr_number));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test PR/MR URL with special characters in owner/repo names
|
* Test PR/MR URL with special characters in owner/repo names
|
||||||
*/
|
*/
|
||||||
TEST(GitPlatformClientTest, ParsePRUrl_SpecialCharacters) {
|
TEST(GitPlatformClientTest, ParsePRUrl_SpecialCharacters) {
|
||||||
GitPlatform platform;
|
GitPlatform platform;
|
||||||
std::string owner, repo;
|
std::string owner, repo;
|
||||||
int pr_number;
|
int pr_number;
|
||||||
|
|
||||||
// GitHub: Underscores and hyphens
|
// GitHub: Underscores and hyphens
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(parse_pr_url("https://github.com/my-owner_123/my-repo_456/pull/999", platform, owner, repo, pr_number));
|
||||||
parse_pr_url("https://github.com/my-owner_123/my-repo_456/pull/999",
|
EXPECT_EQ(platform, GitPlatform::GitHub);
|
||||||
platform, owner, repo, pr_number));
|
EXPECT_EQ(owner, "my-owner_123");
|
||||||
EXPECT_EQ(platform, GitPlatform::GitHub);
|
EXPECT_EQ(repo, "my-repo_456");
|
||||||
EXPECT_EQ(owner, "my-owner_123");
|
EXPECT_EQ(pr_number, 999);
|
||||||
EXPECT_EQ(repo, "my-repo_456");
|
|
||||||
EXPECT_EQ(pr_number, 999);
|
|
||||||
|
|
||||||
// GitLab: Complex group paths
|
// GitLab: Complex group paths
|
||||||
ASSERT_TRUE(parse_pr_url(
|
ASSERT_TRUE(parse_pr_url("https://gitlab.com/org-name/team-1/my_project/-/merge_requests/100", platform, owner, repo, pr_number));
|
||||||
"https://gitlab.com/org-name/team-1/my_project/-/merge_requests/100",
|
EXPECT_EQ(platform, GitPlatform::GitLab);
|
||||||
platform, owner, repo, pr_number));
|
EXPECT_EQ(owner, "org-name/team-1");
|
||||||
EXPECT_EQ(platform, GitPlatform::GitLab);
|
EXPECT_EQ(repo, "my_project");
|
||||||
EXPECT_EQ(owner, "org-name/team-1");
|
EXPECT_EQ(pr_number, 100);
|
||||||
EXPECT_EQ(repo, "my_project");
|
|
||||||
EXPECT_EQ(pr_number, 100);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,228 +12,257 @@ using namespace wizardmerge::analysis;
|
|||||||
* Test risk level to string conversion
|
* Test risk level to string conversion
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, RiskLevelToString) {
|
TEST(RiskAnalyzerTest, RiskLevelToString) {
|
||||||
EXPECT_EQ(risk_level_to_string(RiskLevel::LOW), "low");
|
EXPECT_EQ(risk_level_to_string(RiskLevel::LOW), "low");
|
||||||
EXPECT_EQ(risk_level_to_string(RiskLevel::MEDIUM), "medium");
|
EXPECT_EQ(risk_level_to_string(RiskLevel::MEDIUM), "medium");
|
||||||
EXPECT_EQ(risk_level_to_string(RiskLevel::HIGH), "high");
|
EXPECT_EQ(risk_level_to_string(RiskLevel::HIGH), "high");
|
||||||
EXPECT_EQ(risk_level_to_string(RiskLevel::CRITICAL), "critical");
|
EXPECT_EQ(risk_level_to_string(RiskLevel::CRITICAL), "critical");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test basic risk analysis for "ours"
|
* Test basic risk analysis for "ours"
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, BasicRiskAnalysisOurs) {
|
TEST(RiskAnalyzerTest, BasicRiskAnalysisOurs) {
|
||||||
std::vector<std::string> base = {"int x = 10;"};
|
std::vector<std::string> base = {"int x = 10;"};
|
||||||
std::vector<std::string> ours = {"int x = 20;"};
|
std::vector<std::string> ours = {"int x = 20;"};
|
||||||
std::vector<std::string> theirs = {"int x = 30;"};
|
std::vector<std::string> theirs = {"int x = 30;"};
|
||||||
|
|
||||||
auto risk = analyze_risk_ours(base, ours, theirs);
|
auto risk = analyze_risk_ours(base, ours, theirs);
|
||||||
|
|
||||||
EXPECT_TRUE(risk.level == RiskLevel::LOW || risk.level == RiskLevel::MEDIUM);
|
EXPECT_TRUE(risk.level == RiskLevel::LOW || risk.level == RiskLevel::MEDIUM);
|
||||||
EXPECT_GE(risk.confidence_score, 0.0);
|
EXPECT_GE(risk.confidence_score, 0.0);
|
||||||
EXPECT_LE(risk.confidence_score, 1.0);
|
EXPECT_LE(risk.confidence_score, 1.0);
|
||||||
EXPECT_FALSE(risk.recommendations.empty());
|
EXPECT_FALSE(risk.recommendations.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test basic risk analysis for "theirs"
|
* Test basic risk analysis for "theirs"
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, BasicRiskAnalysisTheirs) {
|
TEST(RiskAnalyzerTest, BasicRiskAnalysisTheirs) {
|
||||||
std::vector<std::string> base = {"int x = 10;"};
|
std::vector<std::string> base = {"int x = 10;"};
|
||||||
std::vector<std::string> ours = {"int x = 20;"};
|
std::vector<std::string> ours = {"int x = 20;"};
|
||||||
std::vector<std::string> theirs = {"int x = 30;"};
|
std::vector<std::string> theirs = {"int x = 30;"};
|
||||||
|
|
||||||
auto risk = analyze_risk_theirs(base, ours, theirs);
|
auto risk = analyze_risk_theirs(base, ours, theirs);
|
||||||
|
|
||||||
EXPECT_TRUE(risk.level == RiskLevel::LOW || risk.level == RiskLevel::MEDIUM);
|
EXPECT_TRUE(risk.level == RiskLevel::LOW || risk.level == RiskLevel::MEDIUM);
|
||||||
EXPECT_GE(risk.confidence_score, 0.0);
|
EXPECT_GE(risk.confidence_score, 0.0);
|
||||||
EXPECT_LE(risk.confidence_score, 1.0);
|
EXPECT_LE(risk.confidence_score, 1.0);
|
||||||
EXPECT_FALSE(risk.recommendations.empty());
|
EXPECT_FALSE(risk.recommendations.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test risk analysis for "both" (concatenation)
|
* Test risk analysis for "both" (concatenation)
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, RiskAnalysisBoth) {
|
TEST(RiskAnalyzerTest, RiskAnalysisBoth) {
|
||||||
std::vector<std::string> base = {"int x = 10;"};
|
std::vector<std::string> base = {"int x = 10;"};
|
||||||
std::vector<std::string> ours = {"int x = 20;"};
|
std::vector<std::string> ours = {"int x = 20;"};
|
||||||
std::vector<std::string> theirs = {"int x = 30;"};
|
std::vector<std::string> theirs = {"int x = 30;"};
|
||||||
|
|
||||||
auto risk = analyze_risk_both(base, ours, theirs);
|
auto risk = analyze_risk_both(base, ours, theirs);
|
||||||
|
|
||||||
// "Both" strategy should typically have medium or higher risk
|
// "Both" strategy should typically have medium or higher risk
|
||||||
EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM);
|
EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM);
|
||||||
EXPECT_GE(risk.confidence_score, 0.0);
|
EXPECT_GE(risk.confidence_score, 0.0);
|
||||||
EXPECT_LE(risk.confidence_score, 1.0);
|
EXPECT_LE(risk.confidence_score, 1.0);
|
||||||
EXPECT_FALSE(risk.recommendations.empty());
|
EXPECT_FALSE(risk.recommendations.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test critical pattern detection
|
* Test critical pattern detection
|
||||||
*/
|
*/
|
||||||
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;",
|
std::vector<std::string> unsafe_code = {"delete ptr;", "system(\"rm -rf /\");"};
|
||||||
"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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test API signature change detection
|
* Test API signature change detection
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, DetectAPISignatureChanges) {
|
TEST(RiskAnalyzerTest, DetectAPISignatureChanges) {
|
||||||
std::vector<std::string> base_sig = {"void myFunction(int x) {"};
|
std::vector<std::string> base_sig = {"void myFunction(int x) {"};
|
||||||
std::vector<std::string> modified_sig = {"void myFunction(int x, int y) {"};
|
std::vector<std::string> modified_sig = {"void myFunction(int x, int y) {"};
|
||||||
std::vector<std::string> same_sig = {"void myFunction(int x) {"};
|
std::vector<std::string> same_sig = {"void myFunction(int x) {"};
|
||||||
|
|
||||||
EXPECT_TRUE(has_api_signature_changes(base_sig, modified_sig));
|
EXPECT_TRUE(has_api_signature_changes(base_sig, modified_sig));
|
||||||
EXPECT_FALSE(has_api_signature_changes(base_sig, same_sig));
|
EXPECT_FALSE(has_api_signature_changes(base_sig, same_sig));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test high risk for large changes
|
* Test high risk for large changes
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, HighRiskForLargeChanges) {
|
TEST(RiskAnalyzerTest, HighRiskForLargeChanges) {
|
||||||
std::vector<std::string> base = {"line1"};
|
std::vector<std::string> base = {"line1"};
|
||||||
std::vector<std::string> ours;
|
std::vector<std::string> ours;
|
||||||
std::vector<std::string> theirs = {"line1"};
|
std::vector<std::string> theirs = {"line1"};
|
||||||
|
|
||||||
// Create large change in ours
|
// Create large change in ours
|
||||||
for (int i = 0; i < 15; ++i) {
|
for (int i = 0; i < 15; ++i) {
|
||||||
ours.push_back("changed_line_" + std::to_string(i));
|
ours.push_back("changed_line_" + std::to_string(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto risk = analyze_risk_ours(base, ours, theirs);
|
auto risk = analyze_risk_ours(base, ours, theirs);
|
||||||
|
|
||||||
// Should detect significant changes
|
// Should detect significant changes
|
||||||
EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM);
|
EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM);
|
||||||
EXPECT_FALSE(risk.risk_factors.empty());
|
EXPECT_FALSE(risk.risk_factors.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test risk with critical patterns
|
* Test risk with critical patterns
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, CriticalPatternsIncreaseRisk) {
|
TEST(RiskAnalyzerTest, CriticalPatternsIncreaseRisk) {
|
||||||
std::vector<std::string> base = {"int x = 10;"};
|
std::vector<std::string> base = {"int x = 10;"};
|
||||||
std::vector<std::string> ours = {"delete database;", "eval(user_input);"};
|
std::vector<std::string> ours = {"delete database;", "eval(user_input);"};
|
||||||
std::vector<std::string> theirs = {"int x = 10;"};
|
std::vector<std::string> theirs = {"int x = 10;"};
|
||||||
|
|
||||||
auto risk = analyze_risk_ours(base, ours, theirs);
|
auto risk = analyze_risk_ours(base, ours, theirs);
|
||||||
|
|
||||||
EXPECT_TRUE(risk.level >= RiskLevel::HIGH);
|
EXPECT_TRUE(risk.level >= RiskLevel::HIGH);
|
||||||
EXPECT_TRUE(risk.affects_critical_section);
|
EXPECT_TRUE(risk.affects_critical_section);
|
||||||
EXPECT_FALSE(risk.risk_factors.empty());
|
EXPECT_FALSE(risk.risk_factors.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test risk factors are populated
|
* Test risk factors are populated
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, RiskFactorsPopulated) {
|
TEST(RiskAnalyzerTest, RiskFactorsPopulated) {
|
||||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||||
std::vector<std::string> ours = {"changed1", "changed2", "changed3"};
|
std::vector<std::string> ours = {"changed1", "changed2", "changed3"};
|
||||||
std::vector<std::string> theirs = {"line1", "line2", "line3"};
|
std::vector<std::string> theirs = {"line1", "line2", "line3"};
|
||||||
|
|
||||||
auto risk = analyze_risk_ours(base, ours, theirs);
|
auto risk = analyze_risk_ours(base, ours, theirs);
|
||||||
|
|
||||||
// Should have some analysis results
|
// Should have some analysis results
|
||||||
EXPECT_TRUE(!risk.recommendations.empty() || !risk.risk_factors.empty());
|
EXPECT_TRUE(!risk.recommendations.empty() || !risk.risk_factors.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test TypeScript interface change detection
|
* Test TypeScript interface change detection
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, TypeScriptInterfaceChangesDetected) {
|
TEST(RiskAnalyzerTest, TypeScriptInterfaceChangesDetected) {
|
||||||
std::vector<std::string> base = {"interface User {", " name: string;",
|
std::vector<std::string> base = {
|
||||||
"}"};
|
"interface User {",
|
||||||
std::vector<std::string> modified = {"interface User {", " name: string;",
|
" 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test TypeScript type alias change detection
|
* Test TypeScript type alias change detection
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, TypeScriptTypeChangesDetected) {
|
TEST(RiskAnalyzerTest, TypeScriptTypeChangesDetected) {
|
||||||
std::vector<std::string> base = {"type Status = 'pending' | 'approved';"};
|
std::vector<std::string> base = {
|
||||||
std::vector<std::string> modified = {
|
"type Status = 'pending' | 'approved';"
|
||||||
"type Status = 'pending' | 'approved' | 'rejected';"};
|
};
|
||||||
|
std::vector<std::string> modified = {
|
||||||
|
"type Status = 'pending' | 'approved' | 'rejected';"
|
||||||
|
};
|
||||||
|
|
||||||
EXPECT_TRUE(has_typescript_interface_changes(base, modified));
|
EXPECT_TRUE(has_typescript_interface_changes(base, modified));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test TypeScript enum change detection
|
* Test TypeScript enum change detection
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, TypeScriptEnumChangesDetected) {
|
TEST(RiskAnalyzerTest, TypeScriptEnumChangesDetected) {
|
||||||
std::vector<std::string> base = {"enum Color {", " Red,", " Green",
|
std::vector<std::string> base = {
|
||||||
"}"};
|
"enum Color {",
|
||||||
std::vector<std::string> modified = {"enum Color {", " Red,", " Green,",
|
" Red,",
|
||||||
" Blue", "}"};
|
" Green",
|
||||||
|
"}"
|
||||||
|
};
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test package-lock.json file detection
|
* Test package-lock.json file detection
|
||||||
*/
|
*/
|
||||||
TEST(RiskAnalyzerTest, PackageLockFileDetection) {
|
TEST(RiskAnalyzerTest, PackageLockFileDetection) {
|
||||||
EXPECT_TRUE(is_package_lock_file("package-lock.json"));
|
EXPECT_TRUE(is_package_lock_file("package-lock.json"));
|
||||||
EXPECT_TRUE(is_package_lock_file("path/to/package-lock.json"));
|
EXPECT_TRUE(is_package_lock_file("path/to/package-lock.json"));
|
||||||
EXPECT_TRUE(is_package_lock_file("yarn.lock"));
|
EXPECT_TRUE(is_package_lock_file("yarn.lock"));
|
||||||
EXPECT_TRUE(is_package_lock_file("pnpm-lock.yaml"));
|
EXPECT_TRUE(is_package_lock_file("pnpm-lock.yaml"));
|
||||||
EXPECT_TRUE(is_package_lock_file("bun.lockb"));
|
EXPECT_TRUE(is_package_lock_file("bun.lockb"));
|
||||||
EXPECT_FALSE(is_package_lock_file("package.json"));
|
EXPECT_FALSE(is_package_lock_file("package.json"));
|
||||||
EXPECT_FALSE(is_package_lock_file("src/index.ts"));
|
EXPECT_FALSE(is_package_lock_file("src/index.ts"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test TypeScript critical patterns detection
|
* Test TypeScript critical patterns detection
|
||||||
*/
|
*/
|
||||||
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;", "// @ts-ignore",
|
"const user = data as any;",
|
||||||
"element.innerHTML = userInput;",
|
"// @ts-ignore",
|
||||||
"localStorage.setItem('password', pwd);"};
|
"element.innerHTML = userInput;",
|
||||||
|
"localStorage.setItem('password', pwd);"
|
||||||
|
};
|
||||||
|
|
||||||
EXPECT_TRUE(contains_critical_patterns(code_with_ts_issues));
|
EXPECT_TRUE(contains_critical_patterns(code_with_ts_issues));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test TypeScript safe code doesn't trigger false positives
|
* Test TypeScript safe code doesn't trigger false positives
|
||||||
*/
|
*/
|
||||||
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 {", " return `Hello, ${name}`;",
|
"function greet(name: string): string {",
|
||||||
"}"};
|
" return `Hello, ${name}`;",
|
||||||
|
"}"
|
||||||
|
};
|
||||||
|
|
||||||
EXPECT_FALSE(contains_critical_patterns(safe_code));
|
EXPECT_FALSE(contains_critical_patterns(safe_code));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 = {"interface User {", " name: string;",
|
std::vector<std::string> base = {
|
||||||
"}"};
|
"interface User {",
|
||||||
std::vector<std::string> ours = {"interface User {", " name: string;",
|
" name: string;",
|
||||||
" email: string;", "}"};
|
"}"
|
||||||
std::vector<std::string> theirs = base;
|
};
|
||||||
|
std::vector<std::string> ours = {
|
||||||
|
"interface User {",
|
||||||
|
" name: string;",
|
||||||
|
" email: string;",
|
||||||
|
"}"
|
||||||
|
};
|
||||||
|
std::vector<std::string> theirs = base;
|
||||||
|
|
||||||
auto risk = analyze_risk_ours(base, ours, theirs);
|
auto risk = analyze_risk_ours(base, ours, theirs);
|
||||||
|
|
||||||
EXPECT_TRUE(risk.has_api_changes);
|
EXPECT_TRUE(risk.has_api_changes);
|
||||||
EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM);
|
EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM);
|
||||||
|
|
||||||
// Check if TypeScript-related risk factor is mentioned
|
// Check if TypeScript-related risk factor is mentioned
|
||||||
bool has_ts_risk = false;
|
bool has_ts_risk = false;
|
||||||
for (const auto &factor : risk.risk_factors) {
|
for (const auto& factor : risk.risk_factors) {
|
||||||
if (factor.find("TypeScript") != std::string::npos) {
|
if (factor.find("TypeScript") != std::string::npos) {
|
||||||
has_ts_risk = true;
|
has_ts_risk = true;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
EXPECT_TRUE(has_ts_risk);
|
||||||
EXPECT_TRUE(has_ts_risk);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,114 +12,114 @@ using namespace wizardmerge::merge;
|
|||||||
* Test basic three-way merge with no conflicts
|
* Test basic three-way merge with no conflicts
|
||||||
*/
|
*/
|
||||||
TEST(ThreeWayMergeTest, NoConflicts) {
|
TEST(ThreeWayMergeTest, NoConflicts) {
|
||||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||||
std::vector<std::string> ours = {"line1", "line2_modified", "line3"};
|
std::vector<std::string> ours = {"line1", "line2_modified", "line3"};
|
||||||
std::vector<std::string> theirs = {"line1", "line2", "line3_modified"};
|
std::vector<std::string> theirs = {"line1", "line2", "line3_modified"};
|
||||||
|
|
||||||
auto result = three_way_merge(base, ours, theirs);
|
auto result = three_way_merge(base, ours, theirs);
|
||||||
|
|
||||||
EXPECT_FALSE(result.has_conflicts());
|
EXPECT_FALSE(result.has_conflicts());
|
||||||
ASSERT_EQ(result.merged_lines.size(), 3);
|
ASSERT_EQ(result.merged_lines.size(), 3);
|
||||||
EXPECT_EQ(result.merged_lines[0].content, "line1");
|
EXPECT_EQ(result.merged_lines[0].content, "line1");
|
||||||
EXPECT_EQ(result.merged_lines[1].content, "line2_modified");
|
EXPECT_EQ(result.merged_lines[1].content, "line2_modified");
|
||||||
EXPECT_EQ(result.merged_lines[2].content, "line3_modified");
|
EXPECT_EQ(result.merged_lines[2].content, "line3_modified");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test three-way merge with conflicts
|
* Test three-way merge with conflicts
|
||||||
*/
|
*/
|
||||||
TEST(ThreeWayMergeTest, WithConflicts) {
|
TEST(ThreeWayMergeTest, WithConflicts) {
|
||||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||||
std::vector<std::string> ours = {"line1", "line2_ours", "line3"};
|
std::vector<std::string> ours = {"line1", "line2_ours", "line3"};
|
||||||
std::vector<std::string> theirs = {"line1", "line2_theirs", "line3"};
|
std::vector<std::string> theirs = {"line1", "line2_theirs", "line3"};
|
||||||
|
|
||||||
auto result = three_way_merge(base, ours, theirs);
|
auto result = three_way_merge(base, ours, theirs);
|
||||||
|
|
||||||
EXPECT_TRUE(result.has_conflicts());
|
EXPECT_TRUE(result.has_conflicts());
|
||||||
EXPECT_EQ(result.conflicts.size(), 1);
|
EXPECT_EQ(result.conflicts.size(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test identical changes from both sides
|
* Test identical changes from both sides
|
||||||
*/
|
*/
|
||||||
TEST(ThreeWayMergeTest, IdenticalChanges) {
|
TEST(ThreeWayMergeTest, IdenticalChanges) {
|
||||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||||
std::vector<std::string> ours = {"line1", "line2_same", "line3"};
|
std::vector<std::string> ours = {"line1", "line2_same", "line3"};
|
||||||
std::vector<std::string> theirs = {"line1", "line2_same", "line3"};
|
std::vector<std::string> theirs = {"line1", "line2_same", "line3"};
|
||||||
|
|
||||||
auto result = three_way_merge(base, ours, theirs);
|
auto result = three_way_merge(base, ours, theirs);
|
||||||
|
|
||||||
EXPECT_FALSE(result.has_conflicts());
|
EXPECT_FALSE(result.has_conflicts());
|
||||||
EXPECT_EQ(result.merged_lines[1].content, "line2_same");
|
EXPECT_EQ(result.merged_lines[1].content, "line2_same");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test base equals ours, theirs changed
|
* Test base equals ours, theirs changed
|
||||||
*/
|
*/
|
||||||
TEST(ThreeWayMergeTest, BaseEqualsOurs) {
|
TEST(ThreeWayMergeTest, BaseEqualsOurs) {
|
||||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||||
std::vector<std::string> ours = {"line1", "line2", "line3"};
|
std::vector<std::string> ours = {"line1", "line2", "line3"};
|
||||||
std::vector<std::string> theirs = {"line1", "line2_changed", "line3"};
|
std::vector<std::string> theirs = {"line1", "line2_changed", "line3"};
|
||||||
|
|
||||||
auto result = three_way_merge(base, ours, theirs);
|
auto result = three_way_merge(base, ours, theirs);
|
||||||
|
|
||||||
EXPECT_FALSE(result.has_conflicts());
|
EXPECT_FALSE(result.has_conflicts());
|
||||||
EXPECT_EQ(result.merged_lines[1].content, "line2_changed");
|
EXPECT_EQ(result.merged_lines[1].content, "line2_changed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test base equals theirs, ours changed
|
* Test base equals theirs, ours changed
|
||||||
*/
|
*/
|
||||||
TEST(ThreeWayMergeTest, BaseEqualsTheirs) {
|
TEST(ThreeWayMergeTest, BaseEqualsTheirs) {
|
||||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||||
std::vector<std::string> ours = {"line1", "line2_changed", "line3"};
|
std::vector<std::string> ours = {"line1", "line2_changed", "line3"};
|
||||||
std::vector<std::string> theirs = {"line1", "line2", "line3"};
|
std::vector<std::string> theirs = {"line1", "line2", "line3"};
|
||||||
|
|
||||||
auto result = three_way_merge(base, ours, theirs);
|
auto result = three_way_merge(base, ours, theirs);
|
||||||
|
|
||||||
EXPECT_FALSE(result.has_conflicts());
|
EXPECT_FALSE(result.has_conflicts());
|
||||||
EXPECT_EQ(result.merged_lines[1].content, "line2_changed");
|
EXPECT_EQ(result.merged_lines[1].content, "line2_changed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test auto-resolve whitespace differences
|
* Test auto-resolve whitespace differences
|
||||||
*/
|
*/
|
||||||
TEST(AutoResolveTest, WhitespaceOnly) {
|
TEST(AutoResolveTest, WhitespaceOnly) {
|
||||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||||
std::vector<std::string> ours = {"line1", " line2_changed ", "line3"};
|
std::vector<std::string> ours = {"line1", " line2_changed ", "line3"};
|
||||||
std::vector<std::string> theirs = {"line1", "line2_changed", "line3"};
|
std::vector<std::string> theirs = {"line1", "line2_changed", "line3"};
|
||||||
|
|
||||||
auto result = three_way_merge(base, ours, theirs);
|
auto result = three_way_merge(base, ours, theirs);
|
||||||
auto resolved = auto_resolve(result);
|
auto resolved = auto_resolve(result);
|
||||||
|
|
||||||
// Whitespace-only differences should be auto-resolved
|
// Whitespace-only differences should be auto-resolved
|
||||||
EXPECT_LT(resolved.conflicts.size(), result.conflicts.size());
|
EXPECT_LT(resolved.conflicts.size(), result.conflicts.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test empty files
|
* Test empty files
|
||||||
*/
|
*/
|
||||||
TEST(ThreeWayMergeTest, EmptyFiles) {
|
TEST(ThreeWayMergeTest, EmptyFiles) {
|
||||||
std::vector<std::string> base = {};
|
std::vector<std::string> base = {};
|
||||||
std::vector<std::string> ours = {};
|
std::vector<std::string> ours = {};
|
||||||
std::vector<std::string> theirs = {};
|
std::vector<std::string> theirs = {};
|
||||||
|
|
||||||
auto result = three_way_merge(base, ours, theirs);
|
auto result = three_way_merge(base, ours, theirs);
|
||||||
|
|
||||||
EXPECT_FALSE(result.has_conflicts());
|
EXPECT_FALSE(result.has_conflicts());
|
||||||
EXPECT_EQ(result.merged_lines.size(), 0);
|
EXPECT_EQ(result.merged_lines.size(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test one side adds lines
|
* Test one side adds lines
|
||||||
*/
|
*/
|
||||||
TEST(ThreeWayMergeTest, OneSideAddsLines) {
|
TEST(ThreeWayMergeTest, OneSideAddsLines) {
|
||||||
std::vector<std::string> base = {"line1"};
|
std::vector<std::string> base = {"line1"};
|
||||||
std::vector<std::string> ours = {"line1", "line2"};
|
std::vector<std::string> ours = {"line1", "line2"};
|
||||||
std::vector<std::string> theirs = {"line1"};
|
std::vector<std::string> theirs = {"line1"};
|
||||||
|
|
||||||
auto result = three_way_merge(base, ours, theirs);
|
auto result = three_way_merge(base, ours, theirs);
|
||||||
|
|
||||||
EXPECT_FALSE(result.has_conflicts());
|
EXPECT_FALSE(result.has_conflicts());
|
||||||
ASSERT_EQ(result.merged_lines.size(), 2);
|
ASSERT_EQ(result.merged_lines.size(), 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,37 +9,35 @@
|
|||||||
*/
|
*/
|
||||||
class FileUtils {
|
class FileUtils {
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief Read a file and split into lines
|
* @brief Read a file and split into lines
|
||||||
* @param filePath Path to the file
|
* @param filePath Path to the file
|
||||||
* @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,
|
static bool readLines(const std::string& filePath, std::vector<std::string>& lines);
|
||||||
std::vector<std::string> &lines);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Write lines to a file
|
* @brief Write lines to a file
|
||||||
* @param filePath Path to the file
|
* @param filePath Path to the file
|
||||||
* @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,
|
static bool writeLines(const std::string& filePath, const std::vector<std::string>& lines);
|
||||||
const std::vector<std::string> &lines);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if a file exists
|
* @brief Check if a file exists
|
||||||
* @param filePath Path to the file
|
* @param filePath Path to the file
|
||||||
* @return true if file exists, false otherwise
|
* @return true if file exists, false otherwise
|
||||||
*/
|
*/
|
||||||
static bool fileExists(const std::string &filePath);
|
static bool fileExists(const std::string& filePath);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get file size in bytes
|
* @brief Get file size in bytes
|
||||||
* @param filePath Path to the file
|
* @param filePath Path to the file
|
||||||
* @return File size, or -1 on error
|
* @return File size, or -1 on error
|
||||||
*/
|
*/
|
||||||
static long getFileSize(const std::string &filePath);
|
static long getFileSize(const std::string& filePath);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // FILE_UTILS_H
|
#endif // FILE_UTILS_H
|
||||||
|
|||||||
@@ -1,60 +1,62 @@
|
|||||||
#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
|
||||||
*/
|
*/
|
||||||
class HttpClient {
|
class HttpClient {
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief Construct HTTP client with backend URL
|
* @brief Construct HTTP client with backend URL
|
||||||
* @param backendUrl URL of the backend server (e.g., "http://localhost:8080")
|
* @param backendUrl URL of the backend server (e.g., "http://localhost:8080")
|
||||||
*/
|
*/
|
||||||
explicit HttpClient(const std::string &backendUrl);
|
explicit HttpClient(const std::string& backendUrl);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Perform a three-way merge via backend API
|
* @brief Perform a three-way merge via backend API
|
||||||
* @param base Base version lines
|
* @param base Base version lines
|
||||||
* @param ours Our version lines
|
* @param ours Our version lines
|
||||||
* @param theirs Their version lines
|
* @param theirs Their version lines
|
||||||
* @param merged Output merged lines
|
* @param merged Output merged lines
|
||||||
* @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(const std::vector<std::string> &base,
|
bool performMerge(
|
||||||
const std::vector<std::string> &ours,
|
const std::vector<std::string>& base,
|
||||||
const std::vector<std::string> &theirs,
|
const std::vector<std::string>& ours,
|
||||||
std::vector<std::string> &merged, bool &hasConflicts);
|
const std::vector<std::string>& theirs,
|
||||||
|
std::vector<std::string>& merged,
|
||||||
|
bool& hasConflicts
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if backend is reachable
|
* @brief Check if backend is reachable
|
||||||
* @return true if backend responds, false otherwise
|
* @return true if backend responds, false otherwise
|
||||||
*/
|
*/
|
||||||
bool checkBackend();
|
bool checkBackend();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get last error message
|
* @brief Get last error message
|
||||||
* @return Error message string
|
* @return Error message string
|
||||||
*/
|
*/
|
||||||
std::string getLastError() const { return lastError_; }
|
std::string getLastError() const { return lastError_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string backendUrl_;
|
std::string backendUrl_;
|
||||||
std::string lastError_;
|
std::string lastError_;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Perform HTTP POST request
|
* @brief Perform HTTP POST request
|
||||||
* @param endpoint API endpoint (e.g., "/api/merge")
|
* @param endpoint API endpoint (e.g., "/api/merge")
|
||||||
* @param jsonBody JSON request body
|
* @param jsonBody JSON request body
|
||||||
* @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,
|
bool post(const std::string& endpoint, const std::string& jsonBody, std::string& response);
|
||||||
std::string &response);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // HTTP_CLIENT_H
|
#endif // HTTP_CLIENT_H
|
||||||
|
|||||||
@@ -3,47 +3,45 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
bool FileUtils::readLines(const std::string &filePath,
|
bool FileUtils::readLines(const std::string& filePath, std::vector<std::string>& lines) {
|
||||||
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;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
lines.clear();
|
lines.clear();
|
||||||
std::string line;
|
std::string line;
|
||||||
while (std::getline(file, line)) {
|
while (std::getline(file, line)) {
|
||||||
lines.push_back(line);
|
lines.push_back(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileUtils::writeLines(const std::string &filePath,
|
bool FileUtils::writeLines(const std::string& filePath, const std::vector<std::string>& lines) {
|
||||||
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;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto &line : lines) {
|
for (const auto& line : lines) {
|
||||||
file << line << "\n";
|
file << line << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileUtils::fileExists(const std::string &filePath) {
|
bool FileUtils::fileExists(const std::string& filePath) {
|
||||||
struct stat buffer;
|
struct stat buffer;
|
||||||
return (stat(filePath.c_str(), &buffer) == 0);
|
return (stat(filePath.c_str(), &buffer) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
long FileUtils::getFileSize(const std::string &filePath) {
|
long FileUtils::getFileSize(const std::string& filePath) {
|
||||||
struct stat buffer;
|
struct stat buffer;
|
||||||
if (stat(filePath.c_str(), &buffer) != 0) {
|
if (stat(filePath.c_str(), &buffer) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return buffer.st_size;
|
return buffer.st_size;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,152 +1,142 @@
|
|||||||
#include "http_client.h"
|
#include "http_client.h"
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
// 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,
|
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||||
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) {
|
|
||||||
CURL *curl = curl_easy_init();
|
|
||||||
if (!curl) {
|
|
||||||
lastError_ = "Failed to initialize CURL";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string url = backendUrl_ + endpoint;
|
|
||||||
response.clear();
|
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
|
||||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonBody.c_str());
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
|
||||||
|
|
||||||
struct curl_slist *headers = nullptr;
|
|
||||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
|
||||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
|
||||||
|
|
||||||
CURLcode res = curl_easy_perform(curl);
|
|
||||||
|
|
||||||
bool success = (res == CURLE_OK);
|
|
||||||
if (!success) {
|
|
||||||
lastError_ = std::string("CURL error: ") + curl_easy_strerror(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
curl_slist_free_all(headers);
|
|
||||||
curl_easy_cleanup(curl);
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HttpClient::performMerge(const std::vector<std::string> &base,
|
bool HttpClient::post(const std::string& endpoint, const std::string& jsonBody, std::string& response) {
|
||||||
const std::vector<std::string> &ours,
|
CURL* curl = curl_easy_init();
|
||||||
const std::vector<std::string> &theirs,
|
if (!curl) {
|
||||||
std::vector<std::string> &merged,
|
lastError_ = "Failed to initialize CURL";
|
||||||
bool &hasConflicts) {
|
return false;
|
||||||
// Build JSON request
|
|
||||||
// NOTE: This is a simplified JSON builder for prototype purposes.
|
|
||||||
// LIMITATION: Does not escape special characters in strings (quotes,
|
|
||||||
// backslashes, etc.)
|
|
||||||
// TODO: For production, use a proper JSON library like nlohmann/json or
|
|
||||||
// rapidjson This implementation works for simple test cases but will fail
|
|
||||||
// with complex content.
|
|
||||||
std::ostringstream json;
|
|
||||||
json << "{";
|
|
||||||
json << "\"base\":[";
|
|
||||||
for (size_t i = 0; i < base.size(); ++i) {
|
|
||||||
json << "\"" << base[i] << "\""; // WARNING: No escaping!
|
|
||||||
if (i < base.size() - 1)
|
|
||||||
json << ",";
|
|
||||||
}
|
|
||||||
json << "],";
|
|
||||||
json << "\"ours\":[";
|
|
||||||
for (size_t i = 0; i < ours.size(); ++i) {
|
|
||||||
json << "\"" << ours[i] << "\""; // WARNING: No escaping!
|
|
||||||
if (i < ours.size() - 1)
|
|
||||||
json << ",";
|
|
||||||
}
|
|
||||||
json << "],";
|
|
||||||
json << "\"theirs\":[";
|
|
||||||
for (size_t i = 0; i < theirs.size(); ++i) {
|
|
||||||
json << "\"" << theirs[i] << "\""; // WARNING: No escaping!
|
|
||||||
if (i < theirs.size() - 1)
|
|
||||||
json << ",";
|
|
||||||
}
|
|
||||||
json << "]";
|
|
||||||
json << "}";
|
|
||||||
|
|
||||||
std::string response;
|
|
||||||
if (!post("/api/merge", json.str(), response)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse JSON response (simple parsing for now)
|
|
||||||
// NOTE: This is a fragile string-based parser for prototype purposes.
|
|
||||||
// LIMITATION: Will break on complex JSON or unexpected formatting.
|
|
||||||
// TODO: For production, use a proper JSON library like nlohmann/json or
|
|
||||||
// rapidjson
|
|
||||||
merged.clear();
|
|
||||||
hasConflicts = (response.find("\"has_conflicts\":true") != std::string::npos);
|
|
||||||
|
|
||||||
// Extract merged lines from response
|
|
||||||
// This is a simplified parser - production code MUST use a JSON library
|
|
||||||
size_t mergedPos = response.find("\"merged\":");
|
|
||||||
if (mergedPos != std::string::npos) {
|
|
||||||
size_t startBracket = response.find("[", mergedPos);
|
|
||||||
size_t endBracket = response.find("]", startBracket);
|
|
||||||
if (startBracket != std::string::npos && endBracket != std::string::npos) {
|
|
||||||
std::string mergedArray =
|
|
||||||
response.substr(startBracket + 1, endBracket - startBracket - 1);
|
|
||||||
|
|
||||||
// Parse lines (simplified)
|
|
||||||
size_t pos = 0;
|
|
||||||
while (pos < mergedArray.size()) {
|
|
||||||
size_t quoteStart = mergedArray.find("\"", pos);
|
|
||||||
if (quoteStart == std::string::npos)
|
|
||||||
break;
|
|
||||||
size_t quoteEnd = mergedArray.find("\"", quoteStart + 1);
|
|
||||||
if (quoteEnd == std::string::npos)
|
|
||||||
break;
|
|
||||||
|
|
||||||
std::string line =
|
|
||||||
mergedArray.substr(quoteStart + 1, quoteEnd - quoteStart - 1);
|
|
||||||
merged.push_back(line);
|
|
||||||
pos = quoteEnd + 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
std::string url = backendUrl_ + endpoint;
|
||||||
|
response.clear();
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonBody.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||||
|
|
||||||
|
struct curl_slist* headers = nullptr;
|
||||||
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
|
||||||
|
bool success = (res == CURLE_OK);
|
||||||
|
if (!success) {
|
||||||
|
lastError_ = std::string("CURL error: ") + curl_easy_strerror(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_slist_free_all(headers);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpClient::performMerge(
|
||||||
|
const std::vector<std::string>& base,
|
||||||
|
const std::vector<std::string>& ours,
|
||||||
|
const std::vector<std::string>& theirs,
|
||||||
|
std::vector<std::string>& merged,
|
||||||
|
bool& hasConflicts
|
||||||
|
) {
|
||||||
|
// Build JSON request
|
||||||
|
// NOTE: This is a simplified JSON builder for prototype purposes.
|
||||||
|
// LIMITATION: Does not escape special characters in strings (quotes, backslashes, etc.)
|
||||||
|
// TODO: For production, use a proper JSON library like nlohmann/json or rapidjson
|
||||||
|
// This implementation works for simple test cases but will fail with complex content.
|
||||||
|
std::ostringstream json;
|
||||||
|
json << "{";
|
||||||
|
json << "\"base\":[";
|
||||||
|
for (size_t i = 0; i < base.size(); ++i) {
|
||||||
|
json << "\"" << base[i] << "\""; // WARNING: No escaping!
|
||||||
|
if (i < base.size() - 1) json << ",";
|
||||||
|
}
|
||||||
|
json << "],";
|
||||||
|
json << "\"ours\":[";
|
||||||
|
for (size_t i = 0; i < ours.size(); ++i) {
|
||||||
|
json << "\"" << ours[i] << "\""; // WARNING: No escaping!
|
||||||
|
if (i < ours.size() - 1) json << ",";
|
||||||
|
}
|
||||||
|
json << "],";
|
||||||
|
json << "\"theirs\":[";
|
||||||
|
for (size_t i = 0; i < theirs.size(); ++i) {
|
||||||
|
json << "\"" << theirs[i] << "\""; // WARNING: No escaping!
|
||||||
|
if (i < theirs.size() - 1) json << ",";
|
||||||
|
}
|
||||||
|
json << "]";
|
||||||
|
json << "}";
|
||||||
|
|
||||||
|
std::string response;
|
||||||
|
if (!post("/api/merge", json.str(), response)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON response (simple parsing for now)
|
||||||
|
// NOTE: This is a fragile string-based parser for prototype purposes.
|
||||||
|
// LIMITATION: Will break on complex JSON or unexpected formatting.
|
||||||
|
// TODO: For production, use a proper JSON library like nlohmann/json or rapidjson
|
||||||
|
merged.clear();
|
||||||
|
hasConflicts = (response.find("\"has_conflicts\":true") != std::string::npos);
|
||||||
|
|
||||||
|
// Extract merged lines from response
|
||||||
|
// This is a simplified parser - production code MUST use a JSON library
|
||||||
|
size_t mergedPos = response.find("\"merged\":");
|
||||||
|
if (mergedPos != std::string::npos) {
|
||||||
|
size_t startBracket = response.find("[", mergedPos);
|
||||||
|
size_t endBracket = response.find("]", startBracket);
|
||||||
|
if (startBracket != std::string::npos && endBracket != std::string::npos) {
|
||||||
|
std::string mergedArray = response.substr(startBracket + 1, endBracket - startBracket - 1);
|
||||||
|
|
||||||
|
// Parse lines (simplified)
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos < mergedArray.size()) {
|
||||||
|
size_t quoteStart = mergedArray.find("\"", pos);
|
||||||
|
if (quoteStart == std::string::npos) break;
|
||||||
|
size_t quoteEnd = mergedArray.find("\"", quoteStart + 1);
|
||||||
|
if (quoteEnd == std::string::npos) break;
|
||||||
|
|
||||||
|
std::string line = mergedArray.substr(quoteStart + 1, quoteEnd - quoteStart - 1);
|
||||||
|
merged.push_back(line);
|
||||||
|
pos = quoteEnd + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HttpClient::checkBackend() {
|
bool HttpClient::checkBackend() {
|
||||||
CURL *curl = curl_easy_init();
|
CURL* curl = curl_easy_init();
|
||||||
if (!curl) {
|
if (!curl) {
|
||||||
lastError_ = "Failed to initialize CURL";
|
lastError_ = "Failed to initialize CURL";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string url = backendUrl_ + "/";
|
std::string url = backendUrl_ + "/";
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||||
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
|
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
|
||||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
|
||||||
|
|
||||||
CURLcode res = curl_easy_perform(curl);
|
CURLcode res = curl_easy_perform(curl);
|
||||||
bool success = (res == CURLE_OK);
|
bool success = (res == CURLE_OK);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
lastError_ =
|
lastError_ = std::string("Cannot reach backend: ") + curl_easy_strerror(res);
|
||||||
std::string("Cannot reach backend: ") + curl_easy_strerror(res);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
curl_easy_cleanup(curl);
|
curl_easy_cleanup(curl);
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,423 +1,395 @@
|
|||||||
#include "file_utils.h"
|
|
||||||
#include "http_client.h"
|
#include "http_client.h"
|
||||||
#include <cstdlib>
|
#include "file_utils.h"
|
||||||
#include <cstring>
|
|
||||||
#include <curl/curl.h>
|
|
||||||
#include <fstream>
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
#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
|
std::cout << "WizardMerge CLI Frontend - Intelligent Merge Conflict Resolution\n\n";
|
||||||
<< "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] git-resolve [FILE]\n";
|
||||||
std::cout << " " << programName
|
std::cout << " " << programName << " --help\n";
|
||||||
<< " [OPTIONS] pr-resolve --url <pr_url> [--token <token>]\n";
|
std::cout << " " << programName << " --version\n\n";
|
||||||
std::cout << " " << programName << " [OPTIONS] git-resolve [FILE]\n";
|
std::cout << "Global Options:\n";
|
||||||
std::cout << " " << programName << " --help\n";
|
std::cout << " --backend <url> Backend server URL (default: http://localhost:8080)\n";
|
||||||
std::cout << " " << programName << " --version\n\n";
|
std::cout << " -v, --verbose Enable verbose output\n";
|
||||||
std::cout << "Global Options:\n";
|
std::cout << " -q, --quiet Suppress non-error output\n";
|
||||||
std::cout << " --backend <url> Backend server URL (default: "
|
std::cout << " -h, --help Show this help message\n";
|
||||||
"http://localhost:8080)\n";
|
std::cout << " --version Show version information\n\n";
|
||||||
std::cout << " -v, --verbose Enable verbose output\n";
|
std::cout << "Commands:\n";
|
||||||
std::cout << " -q, --quiet Suppress non-error output\n";
|
std::cout << " merge Perform three-way merge\n";
|
||||||
std::cout << " -h, --help Show this help message\n";
|
std::cout << " --base <file> Base version file (required)\n";
|
||||||
std::cout << " --version Show version information\n\n";
|
std::cout << " --ours <file> Our version file (required)\n";
|
||||||
std::cout << "Commands:\n";
|
std::cout << " --theirs <file> Their version file (required)\n";
|
||||||
std::cout << " merge Perform three-way merge\n";
|
std::cout << " -o, --output <file> Output file (default: stdout)\n";
|
||||||
std::cout << " --base <file> Base version file (required)\n";
|
std::cout << " --format <format> Output format: text, json (default: text)\n\n";
|
||||||
std::cout << " --ours <file> Our version file (required)\n";
|
std::cout << " pr-resolve Resolve pull request conflicts\n";
|
||||||
std::cout << " --theirs <file> Their version file (required)\n";
|
std::cout << " --url <url> Pull request URL (required)\n";
|
||||||
std::cout << " -o, --output <file> Output file (default: stdout)\n";
|
std::cout << " --token <token> GitHub API token (optional, can use GITHUB_TOKEN env)\n";
|
||||||
std::cout << " --format <format> Output format: text, json (default: "
|
std::cout << " --branch <name> Create branch with resolved conflicts (optional)\n";
|
||||||
"text)\n\n";
|
std::cout << " -o, --output <dir> Output directory for resolved files (default: stdout)\n\n";
|
||||||
std::cout << " pr-resolve Resolve pull request conflicts\n";
|
std::cout << " git-resolve Resolve Git merge conflicts (not yet implemented)\n";
|
||||||
std::cout << " --url <url> Pull request URL (required)\n";
|
std::cout << " [FILE] Specific file to resolve (optional)\n\n";
|
||||||
std::cout << " --token <token> GitHub API token (optional, can use "
|
std::cout << "Examples:\n";
|
||||||
"GITHUB_TOKEN env)\n";
|
std::cout << " " << programName << " merge --base base.txt --ours ours.txt --theirs theirs.txt\n";
|
||||||
std::cout << " --branch <name> Create branch with resolved conflicts "
|
std::cout << " " << programName << " merge --base base.txt --ours ours.txt --theirs theirs.txt -o result.txt\n";
|
||||||
"(optional)\n";
|
std::cout << " " << programName << " pr-resolve --url https://github.com/owner/repo/pull/123\n";
|
||||||
std::cout << " -o, --output <dir> Output directory for resolved files "
|
std::cout << " " << programName << " pr-resolve --url https://github.com/owner/repo/pull/123 --token ghp_xxx\n";
|
||||||
"(default: stdout)\n\n";
|
std::cout << " " << programName << " --backend http://remote:8080 merge --base b.txt --ours o.txt --theirs t.txt\n\n";
|
||||||
std::cout << " git-resolve Resolve Git merge conflicts (not yet "
|
|
||||||
"implemented)\n";
|
|
||||||
std::cout << " [FILE] Specific file to resolve (optional)\n\n";
|
|
||||||
std::cout << "Examples:\n";
|
|
||||||
std::cout << " " << programName
|
|
||||||
<< " merge --base base.txt --ours ours.txt --theirs theirs.txt\n";
|
|
||||||
std::cout << " " << programName
|
|
||||||
<< " merge --base base.txt --ours ours.txt --theirs theirs.txt -o "
|
|
||||||
"result.txt\n";
|
|
||||||
std::cout << " " << programName
|
|
||||||
<< " pr-resolve --url https://github.com/owner/repo/pull/123\n";
|
|
||||||
std::cout << " " << programName
|
|
||||||
<< " pr-resolve --url https://github.com/owner/repo/pull/123 "
|
|
||||||
"--token ghp_xxx\n";
|
|
||||||
std::cout << " " << programName
|
|
||||||
<< " --backend http://remote:8080 merge --base b.txt --ours o.txt "
|
|
||||||
"--theirs t.txt\n\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Print version information
|
* @brief Print version information
|
||||||
*/
|
*/
|
||||||
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 "
|
std::cout << "Part of the WizardMerge Intelligent Merge Conflict Resolution system\n";
|
||||||
"system\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Parse command-line arguments and execute merge
|
* @brief Parse command-line arguments and execute merge
|
||||||
*/
|
*/
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
// Default configuration
|
// Default configuration
|
||||||
std::string backendUrl = "http://localhost:8080";
|
std::string backendUrl = "http://localhost:8080";
|
||||||
bool verbose = false;
|
bool verbose = false;
|
||||||
bool quiet = false;
|
bool quiet = false;
|
||||||
std::string command;
|
std::string command;
|
||||||
std::string baseFile, oursFile, theirsFile, outputFile;
|
std::string baseFile, oursFile, theirsFile, outputFile;
|
||||||
std::string format = "text";
|
std::string format = "text";
|
||||||
std::string prUrl, githubToken, branchName;
|
std::string prUrl, githubToken, branchName;
|
||||||
|
|
||||||
// Check environment variable
|
// Check environment variable
|
||||||
const char *envBackend = std::getenv("WIZARDMERGE_BACKEND");
|
const char* envBackend = std::getenv("WIZARDMERGE_BACKEND");
|
||||||
if (envBackend) {
|
if (envBackend) {
|
||||||
backendUrl = envBackend;
|
backendUrl = envBackend;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for GitHub token in environment
|
// Check for GitHub token in environment
|
||||||
const char *envToken = std::getenv("GITHUB_TOKEN");
|
const char* envToken = std::getenv("GITHUB_TOKEN");
|
||||||
if (envToken) {
|
if (envToken) {
|
||||||
githubToken = envToken;
|
githubToken = envToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse arguments
|
// Parse arguments
|
||||||
for (int i = 1; i < argc; ++i) {
|
for (int i = 1; i < argc; ++i) {
|
||||||
std::string arg = argv[i];
|
std::string arg = argv[i];
|
||||||
|
|
||||||
if (arg == "--help" || arg == "-h") {
|
if (arg == "--help" || arg == "-h") {
|
||||||
printUsage(argv[0]);
|
printUsage(argv[0]);
|
||||||
return 0;
|
return 0;
|
||||||
} else if (arg == "--version") {
|
} else if (arg == "--version") {
|
||||||
printVersion();
|
printVersion();
|
||||||
return 0;
|
return 0;
|
||||||
} else if (arg == "--backend") {
|
} else if (arg == "--backend") {
|
||||||
if (i + 1 < argc) {
|
if (i + 1 < argc) {
|
||||||
backendUrl = argv[++i];
|
backendUrl = argv[++i];
|
||||||
} else {
|
} else {
|
||||||
std::cerr << "Error: --backend requires an argument\n";
|
std::cerr << "Error: --backend requires an argument\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
} else if (arg == "--verbose" || arg == "-v") {
|
||||||
|
verbose = true;
|
||||||
|
} else if (arg == "--quiet" || arg == "-q") {
|
||||||
|
quiet = true;
|
||||||
|
} else if (arg == "merge") {
|
||||||
|
command = "merge";
|
||||||
|
} else if (arg == "pr-resolve") {
|
||||||
|
command = "pr-resolve";
|
||||||
|
} else if (arg == "git-resolve") {
|
||||||
|
command = "git-resolve";
|
||||||
|
} else if (arg == "--url") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
prUrl = argv[++i];
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --url requires an argument\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
} else if (arg == "--token") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
githubToken = argv[++i];
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --token requires an argument\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
} else if (arg == "--branch") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
branchName = argv[++i];
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --branch requires an argument\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
} else if (arg == "--base") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
baseFile = argv[++i];
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --base requires an argument\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
} else if (arg == "--ours") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
oursFile = argv[++i];
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --ours requires an argument\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
} else if (arg == "--theirs") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
theirsFile = argv[++i];
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --theirs requires an argument\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
} else if (arg == "--output" || arg == "-o") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
outputFile = argv[++i];
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --output requires an argument\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
} else if (arg == "--format") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
format = argv[++i];
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --format requires an argument\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if command was provided
|
||||||
|
if (command.empty()) {
|
||||||
|
std::cerr << "Error: No command specified\n\n";
|
||||||
|
printUsage(argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
|
||||||
} else if (arg == "--verbose" || arg == "-v") {
|
|
||||||
verbose = true;
|
|
||||||
} else if (arg == "--quiet" || arg == "-q") {
|
|
||||||
quiet = true;
|
|
||||||
} else if (arg == "merge") {
|
|
||||||
command = "merge";
|
|
||||||
} else if (arg == "pr-resolve") {
|
|
||||||
command = "pr-resolve";
|
|
||||||
} else if (arg == "git-resolve") {
|
|
||||||
command = "git-resolve";
|
|
||||||
} else if (arg == "--url") {
|
|
||||||
if (i + 1 < argc) {
|
|
||||||
prUrl = argv[++i];
|
|
||||||
} else {
|
|
||||||
std::cerr << "Error: --url requires an argument\n";
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
} else if (arg == "--token") {
|
|
||||||
if (i + 1 < argc) {
|
|
||||||
githubToken = argv[++i];
|
|
||||||
} else {
|
|
||||||
std::cerr << "Error: --token requires an argument\n";
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
} else if (arg == "--branch") {
|
|
||||||
if (i + 1 < argc) {
|
|
||||||
branchName = argv[++i];
|
|
||||||
} else {
|
|
||||||
std::cerr << "Error: --branch requires an argument\n";
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
} else if (arg == "--base") {
|
|
||||||
if (i + 1 < argc) {
|
|
||||||
baseFile = argv[++i];
|
|
||||||
} else {
|
|
||||||
std::cerr << "Error: --base requires an argument\n";
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
} else if (arg == "--ours") {
|
|
||||||
if (i + 1 < argc) {
|
|
||||||
oursFile = argv[++i];
|
|
||||||
} else {
|
|
||||||
std::cerr << "Error: --ours requires an argument\n";
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
} else if (arg == "--theirs") {
|
|
||||||
if (i + 1 < argc) {
|
|
||||||
theirsFile = argv[++i];
|
|
||||||
} else {
|
|
||||||
std::cerr << "Error: --theirs requires an argument\n";
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
} else if (arg == "--output" || arg == "-o") {
|
|
||||||
if (i + 1 < argc) {
|
|
||||||
outputFile = argv[++i];
|
|
||||||
} else {
|
|
||||||
std::cerr << "Error: --output requires an argument\n";
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
} else if (arg == "--format") {
|
|
||||||
if (i + 1 < argc) {
|
|
||||||
format = argv[++i];
|
|
||||||
} else {
|
|
||||||
std::cerr << "Error: --format requires an argument\n";
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if command was provided
|
|
||||||
if (command.empty()) {
|
|
||||||
std::cerr << "Error: No command specified\n\n";
|
|
||||||
printUsage(argv[0]);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute command
|
|
||||||
if (command == "merge") {
|
|
||||||
// Validate required arguments
|
|
||||||
if (baseFile.empty() || oursFile.empty() || theirsFile.empty()) {
|
|
||||||
std::cerr << "Error: merge command requires --base, --ours, and --theirs "
|
|
||||||
"arguments\n";
|
|
||||||
return 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check files exist
|
// Execute command
|
||||||
if (!FileUtils::fileExists(baseFile)) {
|
if (command == "merge") {
|
||||||
std::cerr << "Error: Base file not found: " << baseFile << "\n";
|
// Validate required arguments
|
||||||
return 4;
|
if (baseFile.empty() || oursFile.empty() || theirsFile.empty()) {
|
||||||
}
|
std::cerr << "Error: merge command requires --base, --ours, and --theirs arguments\n";
|
||||||
if (!FileUtils::fileExists(oursFile)) {
|
return 2;
|
||||||
std::cerr << "Error: Ours file not found: " << oursFile << "\n";
|
}
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
if (!FileUtils::fileExists(theirsFile)) {
|
|
||||||
std::cerr << "Error: Theirs file not found: " << theirsFile << "\n";
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verbose) {
|
// Check files exist
|
||||||
std::cout << "Backend URL: " << backendUrl << "\n";
|
if (!FileUtils::fileExists(baseFile)) {
|
||||||
std::cout << "Base file: " << baseFile << "\n";
|
std::cerr << "Error: Base file not found: " << baseFile << "\n";
|
||||||
std::cout << "Ours file: " << oursFile << "\n";
|
return 4;
|
||||||
std::cout << "Theirs file: " << theirsFile << "\n";
|
}
|
||||||
}
|
if (!FileUtils::fileExists(oursFile)) {
|
||||||
|
std::cerr << "Error: Ours file not found: " << oursFile << "\n";
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
if (!FileUtils::fileExists(theirsFile)) {
|
||||||
|
std::cerr << "Error: Theirs file not found: " << theirsFile << "\n";
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
// Read input files
|
if (verbose) {
|
||||||
std::vector<std::string> baseLines, oursLines, theirsLines;
|
std::cout << "Backend URL: " << backendUrl << "\n";
|
||||||
if (!FileUtils::readLines(baseFile, baseLines)) {
|
std::cout << "Base file: " << baseFile << "\n";
|
||||||
std::cerr << "Error: Failed to read base file\n";
|
std::cout << "Ours file: " << oursFile << "\n";
|
||||||
return 4;
|
std::cout << "Theirs file: " << theirsFile << "\n";
|
||||||
}
|
}
|
||||||
if (!FileUtils::readLines(oursFile, oursLines)) {
|
|
||||||
std::cerr << "Error: Failed to read ours file\n";
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
if (!FileUtils::readLines(theirsFile, theirsLines)) {
|
|
||||||
std::cerr << "Error: Failed to read theirs file\n";
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verbose) {
|
// Read input files
|
||||||
std::cout << "Read " << baseLines.size() << " lines from base\n";
|
std::vector<std::string> baseLines, oursLines, theirsLines;
|
||||||
std::cout << "Read " << oursLines.size() << " lines from ours\n";
|
if (!FileUtils::readLines(baseFile, baseLines)) {
|
||||||
std::cout << "Read " << theirsLines.size() << " lines from theirs\n";
|
std::cerr << "Error: Failed to read base file\n";
|
||||||
}
|
return 4;
|
||||||
|
}
|
||||||
|
if (!FileUtils::readLines(oursFile, oursLines)) {
|
||||||
|
std::cerr << "Error: Failed to read ours file\n";
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
if (!FileUtils::readLines(theirsFile, theirsLines)) {
|
||||||
|
std::cerr << "Error: Failed to read theirs file\n";
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
// Connect to backend and perform merge
|
if (verbose) {
|
||||||
HttpClient client(backendUrl);
|
std::cout << "Read " << baseLines.size() << " lines from base\n";
|
||||||
|
std::cout << "Read " << oursLines.size() << " lines from ours\n";
|
||||||
|
std::cout << "Read " << theirsLines.size() << " lines from theirs\n";
|
||||||
|
}
|
||||||
|
|
||||||
if (!quiet) {
|
// Connect to backend and perform merge
|
||||||
std::cout << "Connecting to backend: " << backendUrl << "\n";
|
HttpClient client(backendUrl);
|
||||||
}
|
|
||||||
|
|
||||||
if (!client.checkBackend()) {
|
if (!quiet) {
|
||||||
std::cerr << "Error: Cannot connect to backend: " << client.getLastError()
|
std::cout << "Connecting to backend: " << backendUrl << "\n";
|
||||||
<< "\n";
|
}
|
||||||
std::cerr << "Make sure the backend server is running on " << backendUrl
|
|
||||||
<< "\n";
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!quiet) {
|
if (!client.checkBackend()) {
|
||||||
std::cout << "Performing three-way merge...\n";
|
std::cerr << "Error: Cannot connect to backend: " << client.getLastError() << "\n";
|
||||||
}
|
std::cerr << "Make sure the backend server is running on " << backendUrl << "\n";
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> mergedLines;
|
if (!quiet) {
|
||||||
bool hasConflicts = false;
|
std::cout << "Performing three-way merge...\n";
|
||||||
|
}
|
||||||
|
|
||||||
if (!client.performMerge(baseLines, oursLines, theirsLines, mergedLines,
|
std::vector<std::string> mergedLines;
|
||||||
hasConflicts)) {
|
bool hasConflicts = false;
|
||||||
std::cerr << "Error: Merge failed: " << client.getLastError() << "\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output results
|
if (!client.performMerge(baseLines, oursLines, theirsLines, mergedLines, hasConflicts)) {
|
||||||
if (!quiet) {
|
std::cerr << "Error: Merge failed: " << client.getLastError() << "\n";
|
||||||
std::cout << "Merge completed. Has conflicts: "
|
return 1;
|
||||||
<< (hasConflicts ? "Yes" : "No") << "\n";
|
}
|
||||||
std::cout << "Result has " << mergedLines.size() << " lines\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write output
|
// Output results
|
||||||
if (outputFile.empty()) {
|
if (!quiet) {
|
||||||
// Write to stdout
|
std::cout << "Merge completed. Has conflicts: " << (hasConflicts ? "Yes" : "No") << "\n";
|
||||||
for (const auto &line : mergedLines) {
|
std::cout << "Result has " << mergedLines.size() << " lines\n";
|
||||||
std::cout << line << "\n";
|
}
|
||||||
}
|
|
||||||
|
// Write output
|
||||||
|
if (outputFile.empty()) {
|
||||||
|
// Write to stdout
|
||||||
|
for (const auto& line : mergedLines) {
|
||||||
|
std::cout << line << "\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!FileUtils::writeLines(outputFile, mergedLines)) {
|
||||||
|
std::cerr << "Error: Failed to write output file\n";
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
if (!quiet) {
|
||||||
|
std::cout << "Output written to: " << outputFile << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasConflicts ? 5 : 0;
|
||||||
|
|
||||||
|
} else if (command == "pr-resolve") {
|
||||||
|
// Validate required arguments
|
||||||
|
if (prUrl.empty()) {
|
||||||
|
std::cerr << "Error: pr-resolve command requires --url argument\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
std::cout << "Backend URL: " << backendUrl << "\n";
|
||||||
|
std::cout << "Pull Request URL: " << prUrl << "\n";
|
||||||
|
if (!githubToken.empty()) {
|
||||||
|
std::cout << "Using GitHub token: " << githubToken.substr(0, 4) << "...\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to backend
|
||||||
|
HttpClient client(backendUrl);
|
||||||
|
|
||||||
|
if (!quiet) {
|
||||||
|
std::cout << "Connecting to backend: " << backendUrl << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client.checkBackend()) {
|
||||||
|
std::cerr << "Error: Cannot connect to backend: " << client.getLastError() << "\n";
|
||||||
|
std::cerr << "Make sure the backend server is running on " << backendUrl << "\n";
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!quiet) {
|
||||||
|
std::cout << "Resolving pull request conflicts...\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build JSON request for PR resolution
|
||||||
|
std::ostringstream json;
|
||||||
|
json << "{";
|
||||||
|
json << "\"pr_url\":\"" << prUrl << "\"";
|
||||||
|
if (!githubToken.empty()) {
|
||||||
|
json << ",\"github_token\":\"" << githubToken << "\"";
|
||||||
|
}
|
||||||
|
if (!branchName.empty()) {
|
||||||
|
json << ",\"create_branch\":true";
|
||||||
|
json << ",\"branch_name\":\"" << branchName << "\"";
|
||||||
|
}
|
||||||
|
json << "}";
|
||||||
|
|
||||||
|
// Perform HTTP POST to /api/pr/resolve
|
||||||
|
std::string response;
|
||||||
|
CURL* curl = curl_easy_init();
|
||||||
|
if (!curl) {
|
||||||
|
std::cerr << "Error: Failed to initialize CURL\n";
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string url = backendUrl + "/api/pr/resolve";
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.str().c_str());
|
||||||
|
|
||||||
|
auto WriteCallback = [](void* contents, size_t size, size_t nmemb, void* userp) -> size_t {
|
||||||
|
((std::string*)userp)->append((char*)contents, size * nmemb);
|
||||||
|
return size * nmemb;
|
||||||
|
};
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +WriteCallback);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||||
|
|
||||||
|
struct curl_slist* headers = nullptr;
|
||||||
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
|
||||||
|
if (res != CURLE_OK) {
|
||||||
|
std::cerr << "Error: Request failed: " << curl_easy_strerror(res) << "\n";
|
||||||
|
curl_slist_free_all(headers);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_slist_free_all(headers);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
|
// Output response
|
||||||
|
if (outputFile.empty()) {
|
||||||
|
std::cout << "\n=== Pull Request Resolution Result ===\n";
|
||||||
|
std::cout << response << "\n";
|
||||||
|
} else {
|
||||||
|
std::ofstream out(outputFile);
|
||||||
|
if (!out) {
|
||||||
|
std::cerr << "Error: Failed to write output file\n";
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
out << response;
|
||||||
|
out.close();
|
||||||
|
if (!quiet) {
|
||||||
|
std::cout << "Result written to: " << outputFile << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if resolution was successful (simple check)
|
||||||
|
if (response.find("\"success\":true") != std::string::npos) {
|
||||||
|
if (!quiet) {
|
||||||
|
std::cout << "\nPull request conflicts resolved successfully!\n";
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
if (!quiet) {
|
||||||
|
std::cerr << "\nFailed to resolve some conflicts. See output for details.\n";
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (command == "git-resolve") {
|
||||||
|
std::cerr << "Error: git-resolve command not yet implemented\n";
|
||||||
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
if (!FileUtils::writeLines(outputFile, mergedLines)) {
|
std::cerr << "Error: Unknown command: " << command << "\n";
|
||||||
std::cerr << "Error: Failed to write output file\n";
|
return 2;
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
if (!quiet) {
|
|
||||||
std::cout << "Output written to: " << outputFile << "\n";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasConflicts ? 5 : 0;
|
return 0;
|
||||||
|
|
||||||
} else if (command == "pr-resolve") {
|
|
||||||
// Validate required arguments
|
|
||||||
if (prUrl.empty()) {
|
|
||||||
std::cerr << "Error: pr-resolve command requires --url argument\n";
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verbose) {
|
|
||||||
std::cout << "Backend URL: " << backendUrl << "\n";
|
|
||||||
std::cout << "Pull Request URL: " << prUrl << "\n";
|
|
||||||
if (!githubToken.empty()) {
|
|
||||||
std::cout << "Using GitHub token: " << githubToken.substr(0, 4)
|
|
||||||
<< "...\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to backend
|
|
||||||
HttpClient client(backendUrl);
|
|
||||||
|
|
||||||
if (!quiet) {
|
|
||||||
std::cout << "Connecting to backend: " << backendUrl << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!client.checkBackend()) {
|
|
||||||
std::cerr << "Error: Cannot connect to backend: " << client.getLastError()
|
|
||||||
<< "\n";
|
|
||||||
std::cerr << "Make sure the backend server is running on " << backendUrl
|
|
||||||
<< "\n";
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!quiet) {
|
|
||||||
std::cout << "Resolving pull request conflicts...\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build JSON request for PR resolution
|
|
||||||
std::ostringstream json;
|
|
||||||
json << "{";
|
|
||||||
json << "\"pr_url\":\"" << prUrl << "\"";
|
|
||||||
if (!githubToken.empty()) {
|
|
||||||
json << ",\"github_token\":\"" << githubToken << "\"";
|
|
||||||
}
|
|
||||||
if (!branchName.empty()) {
|
|
||||||
json << ",\"create_branch\":true";
|
|
||||||
json << ",\"branch_name\":\"" << branchName << "\"";
|
|
||||||
}
|
|
||||||
json << "}";
|
|
||||||
|
|
||||||
// Perform HTTP POST to /api/pr/resolve
|
|
||||||
std::string response;
|
|
||||||
CURL *curl = curl_easy_init();
|
|
||||||
if (!curl) {
|
|
||||||
std::cerr << "Error: Failed to initialize CURL\n";
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string url = backendUrl + "/api/pr/resolve";
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
|
||||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.str().c_str());
|
|
||||||
|
|
||||||
auto WriteCallback = [](void *contents, size_t size, size_t nmemb,
|
|
||||||
void *userp) -> size_t {
|
|
||||||
((std::string *)userp)->append((char *)contents, size * nmemb);
|
|
||||||
return size * nmemb;
|
|
||||||
};
|
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +WriteCallback);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
|
||||||
|
|
||||||
struct curl_slist *headers = nullptr;
|
|
||||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
|
||||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
|
||||||
|
|
||||||
CURLcode res = curl_easy_perform(curl);
|
|
||||||
|
|
||||||
if (res != CURLE_OK) {
|
|
||||||
std::cerr << "Error: Request failed: " << curl_easy_strerror(res) << "\n";
|
|
||||||
curl_slist_free_all(headers);
|
|
||||||
curl_easy_cleanup(curl);
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
curl_slist_free_all(headers);
|
|
||||||
curl_easy_cleanup(curl);
|
|
||||||
|
|
||||||
// Output response
|
|
||||||
if (outputFile.empty()) {
|
|
||||||
std::cout << "\n=== Pull Request Resolution Result ===\n";
|
|
||||||
std::cout << response << "\n";
|
|
||||||
} else {
|
|
||||||
std::ofstream out(outputFile);
|
|
||||||
if (!out) {
|
|
||||||
std::cerr << "Error: Failed to write output file\n";
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
out << response;
|
|
||||||
out.close();
|
|
||||||
if (!quiet) {
|
|
||||||
std::cout << "Result written to: " << outputFile << "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if resolution was successful (simple check)
|
|
||||||
if (response.find("\"success\":true") != std::string::npos) {
|
|
||||||
if (!quiet) {
|
|
||||||
std::cout << "\nPull request conflicts resolved successfully!\n";
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
if (!quiet) {
|
|
||||||
std::cerr
|
|
||||||
<< "\nFailed to resolve some conflicts. See output for details.\n";
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (command == "git-resolve") {
|
|
||||||
std::cerr << "Error: git-resolve command not yet implemented\n";
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
std::cerr << "Error: Unknown command: " << command << "\n";
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,75 +13,76 @@
|
|||||||
* 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);
|
{
|
||||||
app.setApplicationName("WizardMerge");
|
QGuiApplication app(argc, argv);
|
||||||
app.setApplicationVersion("1.0.0");
|
app.setApplicationName("WizardMerge");
|
||||||
app.setOrganizationName("WizardMerge");
|
app.setApplicationVersion("1.0.0");
|
||||||
app.setOrganizationDomain("wizardmerge.dev");
|
app.setOrganizationName("WizardMerge");
|
||||||
|
app.setOrganizationDomain("wizardmerge.dev");
|
||||||
|
|
||||||
// Command line parser
|
// Command line parser
|
||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(
|
parser.setApplicationDescription("WizardMerge - Intelligent Merge Conflict Resolution");
|
||||||
"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)", "url",
|
"Backend server URL (default: http://localhost:8080)",
|
||||||
"http://localhost:8080");
|
"url",
|
||||||
parser.addOption(backendUrlOption);
|
"http://localhost:8080"
|
||||||
|
);
|
||||||
|
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)");
|
||||||
|
|
||||||
parser.process(app);
|
parser.process(app);
|
||||||
|
|
||||||
// Get command line arguments
|
// Get command line arguments
|
||||||
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 =
|
QString filePath = positionalArgs.isEmpty() ? QString() : positionalArgs.first();
|
||||||
positionalArgs.isEmpty() ? QString() : positionalArgs.first();
|
|
||||||
|
|
||||||
// Create QML engine
|
// Create QML engine
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
|
|
||||||
// Expose application settings to QML
|
// Expose application settings to QML
|
||||||
QQmlContext *rootContext = engine.rootContext();
|
QQmlContext* rootContext = engine.rootContext();
|
||||||
rootContext->setContextProperty("backendUrl", backendUrl);
|
rootContext->setContextProperty("backendUrl", backendUrl);
|
||||||
rootContext->setContextProperty("standalone", standalone);
|
rootContext->setContextProperty("standalone", standalone);
|
||||||
rootContext->setContextProperty("initialFile", filePath);
|
rootContext->setContextProperty("initialFile", filePath);
|
||||||
|
|
||||||
// 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(
|
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
|
||||||
&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
|
&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);
|
},
|
||||||
},
|
Qt::QueuedConnection);
|
||||||
Qt::QueuedConnection);
|
|
||||||
|
|
||||||
engine.load(url);
|
engine.load(url);
|
||||||
|
|
||||||
if (engine.rootObjects().isEmpty()) {
|
if (engine.rootObjects().isEmpty()) {
|
||||||
std::cerr << "Error: No root objects loaded from QML" << std::endl;
|
std::cerr << "Error: No root objects loaded from QML" << std::endl;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "WizardMerge Qt6 Frontend Started" << std::endl;
|
std::cout << "WizardMerge Qt6 Frontend Started" << std::endl;
|
||||||
std::cout << "Backend URL: " << backendUrl.toStdString() << std::endl;
|
std::cout << "Backend URL: " << backendUrl.toStdString() << std::endl;
|
||||||
std::cout << "Standalone Mode: " << (standalone ? "Yes" : "No") << std::endl;
|
std::cout << "Standalone Mode: " << (standalone ? "Yes" : "No") << std::endl;
|
||||||
if (!filePath.isEmpty()) {
|
if (!filePath.isEmpty()) {
|
||||||
std::cout << "Opening file: " << filePath.toStdString() << std::endl;
|
std::cout << "Opening file: " << filePath.toStdString() << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.exec();
|
return app.exec();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("\n✓ TLA+ specification parsed successfully!")
|
print(f"\n✓ TLA+ specification parsed successfully!")
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
print("\n✗ TLA+ specification parsing failed")
|
print(f"\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("\n✓ TLC model checking completed successfully!")
|
print(f"\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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user