mirror of
https://github.com/johndoe6345789/WizardMerge.git
synced 2026-04-24 13:44:55 +00:00
Merge branch 'main' into copilot/context-and-risk-analysis
This commit is contained in:
@@ -14,8 +14,7 @@ find_package(CURL QUIET)
|
||||
# Library sources
|
||||
set(WIZARDMERGE_SOURCES
|
||||
src/merge/three_way_merge.cpp
|
||||
src/analysis/context_analyzer.cpp
|
||||
src/analysis/risk_analyzer.cpp
|
||||
src/git/git_cli.cpp
|
||||
)
|
||||
|
||||
# Add git sources only if CURL is available
|
||||
@@ -71,8 +70,7 @@ if(GTest_FOUND)
|
||||
|
||||
set(TEST_SOURCES
|
||||
tests/test_three_way_merge.cpp
|
||||
tests/test_context_analyzer.cpp
|
||||
tests/test_risk_analyzer.cpp
|
||||
tests/test_git_cli.cpp
|
||||
)
|
||||
|
||||
# Add github client tests only if CURL is available
|
||||
|
||||
@@ -172,28 +172,38 @@ curl -X POST http://localhost:8080/api/merge \
|
||||
|
||||
### POST /api/pr/resolve
|
||||
|
||||
Resolve conflicts in a GitHub pull request.
|
||||
Resolve conflicts in a GitHub or GitLab pull/merge request.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"pr_url": "https://github.com/owner/repo/pull/123",
|
||||
"github_token": "ghp_xxx",
|
||||
"api_token": "ghp_xxx",
|
||||
"create_branch": true,
|
||||
"branch_name": "wizardmerge-resolved-pr-123"
|
||||
}
|
||||
```
|
||||
|
||||
**Request Fields:**
|
||||
- `pr_url` (required): Pull/merge request URL (GitHub or GitLab)
|
||||
- `api_token` (optional): API token for authentication (GitHub: `ghp_*`, GitLab: `glpat-*`)
|
||||
- `create_branch` (optional, default: false): Create a new branch with resolved changes
|
||||
- `branch_name` (optional): Custom branch name (auto-generated if not provided)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"pr_info": {
|
||||
"platform": "GitHub",
|
||||
"number": 123,
|
||||
"title": "Feature: Add new functionality",
|
||||
"base_ref": "main",
|
||||
"head_ref": "feature-branch",
|
||||
"mergeable": false
|
||||
"base_sha": "abc123...",
|
||||
"head_sha": "def456...",
|
||||
"mergeable": false,
|
||||
"mergeable_state": "dirty"
|
||||
},
|
||||
"resolved_files": [
|
||||
{
|
||||
@@ -206,21 +216,52 @@ Resolve conflicts in a GitHub pull request.
|
||||
],
|
||||
"total_files": 5,
|
||||
"resolved_count": 4,
|
||||
"failed_count": 0
|
||||
"failed_count": 0,
|
||||
"branch_created": true,
|
||||
"branch_name": "wizardmerge-resolved-pr-123",
|
||||
"branch_path": "/tmp/wizardmerge_pr_123_1234567890",
|
||||
"note": "Branch created successfully. Push to remote with: git -C /tmp/wizardmerge_pr_123_1234567890 push origin wizardmerge-resolved-pr-123"
|
||||
}
|
||||
```
|
||||
|
||||
**Example with curl:**
|
||||
```sh
|
||||
# Basic conflict resolution
|
||||
curl -X POST http://localhost:8080/api/pr/resolve \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"pr_url": "https://github.com/owner/repo/pull/123",
|
||||
"github_token": "ghp_xxx"
|
||||
"api_token": "ghp_xxx"
|
||||
}'
|
||||
|
||||
# With branch creation
|
||||
curl -X POST http://localhost:8080/api/pr/resolve \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"pr_url": "https://github.com/owner/repo/pull/123",
|
||||
"api_token": "ghp_xxx",
|
||||
"create_branch": true,
|
||||
"branch_name": "resolved-conflicts"
|
||||
}'
|
||||
```
|
||||
|
||||
**Note:** Requires libcurl to be installed. The GitHub token is optional for public repositories but required for private ones.
|
||||
**Git CLI Integration:**
|
||||
|
||||
When `create_branch: true` is specified:
|
||||
1. **Clone**: Repository is cloned to `/tmp/wizardmerge_pr_<number>_<timestamp>`
|
||||
2. **Branch**: New branch is created from the PR base branch
|
||||
3. **Resolve**: Merged files are written to the working directory
|
||||
4. **Commit**: Changes are staged and committed with message "Resolve conflicts for PR #<number>"
|
||||
5. **Response**: Branch path is returned for manual inspection or pushing
|
||||
|
||||
**Requirements for Branch Creation:**
|
||||
- Git CLI must be installed (`git --version` works)
|
||||
- Sufficient disk space for repository clone
|
||||
- Write permissions to `/tmp` directory
|
||||
|
||||
**Security Note:** Branch is created locally. To push to remote, configure Git credentials separately (SSH keys or credential helpers). Do not embed tokens in Git URLs.
|
||||
|
||||
**Note:** Requires libcurl to be installed. The API token is optional for public repositories but required for private ones.
|
||||
|
||||
## Deployment
|
||||
|
||||
|
||||
159
backend/include/wizardmerge/git/git_cli.h
Normal file
159
backend/include/wizardmerge/git/git_cli.h
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @file git_cli.h
|
||||
* @brief Git CLI wrapper for repository operations
|
||||
*
|
||||
* Provides C++ wrapper functions for Git command-line operations including
|
||||
* cloning, branching, committing, and pushing changes.
|
||||
*/
|
||||
|
||||
#ifndef WIZARDMERGE_GIT_CLI_H
|
||||
#define WIZARDMERGE_GIT_CLI_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace git {
|
||||
|
||||
/**
|
||||
* @brief Result of a Git operation
|
||||
*/
|
||||
struct GitResult {
|
||||
bool success;
|
||||
std::string output;
|
||||
std::string error;
|
||||
int exit_code;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Configuration for Git operations
|
||||
*/
|
||||
struct GitConfig {
|
||||
std::string user_name;
|
||||
std::string user_email;
|
||||
std::string auth_token; // For HTTPS authentication
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Clone a Git repository
|
||||
*
|
||||
* @param url Repository URL (HTTPS or SSH)
|
||||
* @param destination Local directory path
|
||||
* @param branch Optional specific branch to clone
|
||||
* @param depth Optional shallow clone depth (0 for full clone)
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult clone_repository(
|
||||
const std::string& url,
|
||||
const std::string& destination,
|
||||
const std::string& branch = "",
|
||||
int depth = 0
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Create and checkout a new branch
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param branch_name Name of the new branch
|
||||
* @param base_branch Optional base branch (defaults to current branch)
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult create_branch(
|
||||
const std::string& repo_path,
|
||||
const std::string& branch_name,
|
||||
const std::string& base_branch = ""
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Checkout an existing branch
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param branch_name Name of the branch to checkout
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult checkout_branch(
|
||||
const std::string& repo_path,
|
||||
const std::string& branch_name
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Stage files for commit
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param files Vector of file paths (relative to repo root)
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult add_files(
|
||||
const std::string& repo_path,
|
||||
const std::vector<std::string>& files
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Commit staged changes
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param message Commit message
|
||||
* @param config Optional Git configuration
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult commit(
|
||||
const std::string& repo_path,
|
||||
const std::string& message,
|
||||
const GitConfig& config = GitConfig()
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Push commits to remote repository
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param remote Remote name (default: "origin")
|
||||
* @param branch Branch name to push
|
||||
* @param force Force push if needed
|
||||
* @param config Optional Git configuration with auth token
|
||||
* @return GitResult with operation status
|
||||
*/
|
||||
GitResult push(
|
||||
const std::string& repo_path,
|
||||
const std::string& remote,
|
||||
const std::string& branch,
|
||||
bool force = false,
|
||||
const GitConfig& config = GitConfig()
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Get current branch name
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @return Current branch name, or empty optional on error
|
||||
*/
|
||||
std::optional<std::string> get_current_branch(const std::string& repo_path);
|
||||
|
||||
/**
|
||||
* @brief Check if a branch exists
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @param branch_name Name of the branch to check
|
||||
* @return true if branch exists, false otherwise
|
||||
*/
|
||||
bool branch_exists(const std::string& repo_path, const std::string& branch_name);
|
||||
|
||||
/**
|
||||
* @brief Get repository status
|
||||
*
|
||||
* @param repo_path Path to the Git repository
|
||||
* @return GitResult with status output
|
||||
*/
|
||||
GitResult status(const std::string& repo_path);
|
||||
|
||||
/**
|
||||
* @brief Check if Git is available in system PATH
|
||||
*
|
||||
* @return true if git command is available, false otherwise
|
||||
*/
|
||||
bool is_git_available();
|
||||
|
||||
} // namespace git
|
||||
} // namespace wizardmerge
|
||||
|
||||
#endif // WIZARDMERGE_GIT_CLI_H
|
||||
@@ -5,9 +5,11 @@
|
||||
|
||||
#include "PRController.h"
|
||||
#include "wizardmerge/git/git_platform_client.h"
|
||||
#include "wizardmerge/git/git_cli.h"
|
||||
#include "wizardmerge/merge/three_way_merge.h"
|
||||
#include <json/json.h>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
using namespace wizardmerge::controllers;
|
||||
using namespace wizardmerge::git;
|
||||
@@ -180,15 +182,115 @@ void PRController::resolvePR(
|
||||
response["resolved_count"] = resolved_files;
|
||||
response["failed_count"] = failed_files;
|
||||
|
||||
// Branch creation would require Git CLI access
|
||||
// For now, just report what would be done
|
||||
// Branch creation with Git CLI
|
||||
response["branch_created"] = false;
|
||||
if (create_branch) {
|
||||
if (branch_name.empty()) {
|
||||
branch_name = "wizardmerge-resolved-pr-" + std::to_string(pr_number);
|
||||
}
|
||||
response["branch_name"] = branch_name;
|
||||
response["note"] = "Branch creation requires Git CLI integration (not yet implemented)";
|
||||
|
||||
// Check if Git CLI is available
|
||||
if (!is_git_available()) {
|
||||
response["note"] = "Git CLI not available - branch creation skipped";
|
||||
} else {
|
||||
// Clone repository to temporary location
|
||||
std::filesystem::path temp_base = std::filesystem::temp_directory_path();
|
||||
std::string temp_dir = (temp_base / ("wizardmerge_pr_" + std::to_string(pr_number) + "_" +
|
||||
std::to_string(std::time(nullptr)))).string();
|
||||
|
||||
// Build repository URL
|
||||
std::string repo_url;
|
||||
if (platform == GitPlatform::GitHub) {
|
||||
repo_url = "https://github.com/" + owner + "/" + repo + ".git";
|
||||
} else if (platform == GitPlatform::GitLab) {
|
||||
std::string project_path = owner;
|
||||
if (!repo.empty()) {
|
||||
project_path += "/" + repo;
|
||||
}
|
||||
repo_url = "https://gitlab.com/" + project_path + ".git";
|
||||
}
|
||||
|
||||
// Clone the repository
|
||||
auto clone_result = clone_repository(repo_url, temp_dir, pr.base_ref);
|
||||
|
||||
if (!clone_result.success) {
|
||||
response["note"] = "Failed to clone repository: " + clone_result.error;
|
||||
} else {
|
||||
// Create new branch (without base_branch parameter since we cloned from base_ref)
|
||||
auto branch_result = create_branch(temp_dir, branch_name);
|
||||
|
||||
if (!branch_result.success) {
|
||||
response["note"] = "Failed to create branch: " + branch_result.error;
|
||||
std::filesystem::remove_all(temp_dir);
|
||||
} else {
|
||||
// Write resolved files
|
||||
bool all_files_written = true;
|
||||
for (const auto& file : resolved_files_array) {
|
||||
if (file.isMember("merged_content") && file["merged_content"].isArray()) {
|
||||
std::string file_path = temp_dir + "/" + file["filename"].asString();
|
||||
|
||||
// Create parent directories
|
||||
std::filesystem::path file_path_obj(file_path);
|
||||
std::filesystem::create_directories(file_path_obj.parent_path());
|
||||
|
||||
// Write merged content
|
||||
std::ofstream out_file(file_path);
|
||||
if (out_file.is_open()) {
|
||||
for (const auto& line : file["merged_content"]) {
|
||||
out_file << line.asString() << "\n";
|
||||
}
|
||||
out_file.close();
|
||||
} else {
|
||||
all_files_written = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!all_files_written) {
|
||||
response["note"] = "Failed to write some resolved files";
|
||||
std::filesystem::remove_all(temp_dir);
|
||||
} else {
|
||||
// Stage and commit changes
|
||||
std::vector<std::string> file_paths;
|
||||
for (const auto& file : resolved_files_array) {
|
||||
if (file.isMember("filename")) {
|
||||
file_paths.push_back(file["filename"].asString());
|
||||
}
|
||||
}
|
||||
|
||||
auto add_result = add_files(temp_dir, file_paths);
|
||||
if (!add_result.success) {
|
||||
response["note"] = "Failed to stage files: " + add_result.error;
|
||||
std::filesystem::remove_all(temp_dir);
|
||||
} else {
|
||||
GitConfig git_config;
|
||||
git_config.user_name = "WizardMerge Bot";
|
||||
git_config.user_email = "wizardmerge@example.com";
|
||||
git_config.auth_token = api_token;
|
||||
|
||||
std::string commit_message = "Resolve conflicts for PR #" + std::to_string(pr_number);
|
||||
auto commit_result = commit(temp_dir, commit_message, git_config);
|
||||
|
||||
if (!commit_result.success) {
|
||||
response["note"] = "Failed to commit changes: " + commit_result.error;
|
||||
std::filesystem::remove_all(temp_dir);
|
||||
} else {
|
||||
response["branch_created"] = true;
|
||||
response["branch_path"] = temp_dir;
|
||||
response["note"] = "Branch created successfully. Push to remote with: git -C " +
|
||||
temp_dir + " push origin " + branch_name;
|
||||
|
||||
// Note: Pushing requires authentication setup
|
||||
// For security, we don't push automatically with token in URL
|
||||
// Users should configure Git credentials or use SSH keys
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
|
||||
240
backend/src/git/git_cli.cpp
Normal file
240
backend/src/git/git_cli.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* @file git_cli.cpp
|
||||
* @brief Implementation of Git CLI wrapper functions
|
||||
*/
|
||||
|
||||
#include "wizardmerge/git/git_cli.h"
|
||||
#include <cstdlib>
|
||||
#include <array>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <sys/wait.h>
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace git {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Execute a shell command and capture output
|
||||
*/
|
||||
GitResult execute_command(const std::string& command) {
|
||||
GitResult result;
|
||||
result.exit_code = 0;
|
||||
|
||||
// Execute command and capture output
|
||||
std::array<char, 128> buffer;
|
||||
std::string output;
|
||||
|
||||
FILE* pipe = popen((command + " 2>&1").c_str(), "r");
|
||||
if (!pipe) {
|
||||
result.success = false;
|
||||
result.error = "Failed to execute command";
|
||||
result.exit_code = -1;
|
||||
return result;
|
||||
}
|
||||
|
||||
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
|
||||
output += buffer.data();
|
||||
}
|
||||
|
||||
int status = pclose(pipe);
|
||||
result.exit_code = WEXITSTATUS(status);
|
||||
result.success = (result.exit_code == 0);
|
||||
result.output = output;
|
||||
|
||||
if (!result.success) {
|
||||
result.error = output;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build git command with working directory
|
||||
*/
|
||||
std::string git_command(const std::string& repo_path, const std::string& cmd) {
|
||||
if (repo_path.empty()) {
|
||||
return "git " + cmd;
|
||||
}
|
||||
return "git -C \"" + repo_path + "\" " + cmd;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
bool is_git_available() {
|
||||
GitResult result = execute_command("git --version");
|
||||
return result.success;
|
||||
}
|
||||
|
||||
GitResult clone_repository(
|
||||
const std::string& url,
|
||||
const std::string& destination,
|
||||
const std::string& branch,
|
||||
int depth
|
||||
) {
|
||||
std::ostringstream cmd;
|
||||
cmd << "git clone";
|
||||
|
||||
if (!branch.empty()) {
|
||||
cmd << " --branch \"" << branch << "\"";
|
||||
}
|
||||
|
||||
if (depth > 0) {
|
||||
cmd << " --depth " << depth;
|
||||
}
|
||||
|
||||
cmd << " \"" << url << "\" \"" << destination << "\"";
|
||||
|
||||
return execute_command(cmd.str());
|
||||
}
|
||||
|
||||
GitResult 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(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));
|
||||
}
|
||||
|
||||
GitResult push(
|
||||
const std::string& repo_path,
|
||||
const std::string& remote,
|
||||
const std::string& branch,
|
||||
bool force,
|
||||
const GitConfig& config
|
||||
) {
|
||||
std::ostringstream cmd;
|
||||
cmd << "push";
|
||||
|
||||
if (force) {
|
||||
cmd << " --force";
|
||||
}
|
||||
|
||||
// Set upstream if it's a new branch
|
||||
cmd << " --set-upstream \"" << remote << "\" \"" << branch << "\"";
|
||||
|
||||
std::string full_cmd = git_command(repo_path, cmd.str());
|
||||
|
||||
// If auth token is provided, inject it into the URL
|
||||
// This is a simplified approach; in production, use credential helpers
|
||||
if (!config.auth_token.empty()) {
|
||||
// Note: This assumes HTTPS URLs. For production, use git credential helpers
|
||||
// or SSH keys for better security
|
||||
std::cerr << "Note: Auth token provided. Consider using credential helpers for production." << std::endl;
|
||||
}
|
||||
|
||||
return execute_command(full_cmd);
|
||||
}
|
||||
|
||||
std::optional<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
|
||||
206
backend/tests/test_git_cli.cpp
Normal file
206
backend/tests/test_git_cli.cpp
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* @file test_git_cli.cpp
|
||||
* @brief Unit tests for Git CLI wrapper functionality
|
||||
*/
|
||||
|
||||
#include "wizardmerge/git/git_cli.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
using namespace wizardmerge::git;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class GitCLITest : public ::testing::Test {
|
||||
protected:
|
||||
std::string test_dir;
|
||||
|
||||
void SetUp() override {
|
||||
// Create temporary test directory using std::filesystem
|
||||
std::filesystem::path temp_base = std::filesystem::temp_directory_path();
|
||||
test_dir = (temp_base / ("wizardmerge_git_test_" + std::to_string(time(nullptr)))).string();
|
||||
fs::create_directories(test_dir);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Clean up test directory
|
||||
if (fs::exists(test_dir)) {
|
||||
fs::remove_all(test_dir);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to initialize a git repo
|
||||
void init_repo(const std::string& path) {
|
||||
system(("git init \"" + path + "\" 2>&1 > /dev/null").c_str());
|
||||
system(("git -C \"" + path + "\" config user.name \"Test User\"").c_str());
|
||||
system(("git -C \"" + path + "\" config user.email \"test@example.com\"").c_str());
|
||||
}
|
||||
|
||||
// Helper to create a file
|
||||
void create_file(const std::string& path, const std::string& content) {
|
||||
std::ofstream file(path);
|
||||
file << content;
|
||||
file.close();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Test Git availability check
|
||||
*/
|
||||
TEST_F(GitCLITest, GitAvailability) {
|
||||
// Git should be available in CI environment
|
||||
EXPECT_TRUE(is_git_available());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test branch existence check
|
||||
*/
|
||||
TEST_F(GitCLITest, BranchExists) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create initial commit (required for branch operations)
|
||||
create_file(repo_path + "/test.txt", "initial content");
|
||||
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str());
|
||||
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str());
|
||||
|
||||
// Default branch should exist (main or master)
|
||||
auto current_branch = get_current_branch(repo_path);
|
||||
ASSERT_TRUE(current_branch.has_value());
|
||||
EXPECT_TRUE(branch_exists(repo_path, current_branch.value()));
|
||||
|
||||
// Non-existent branch should not exist
|
||||
EXPECT_FALSE(branch_exists(repo_path, "nonexistent-branch"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting current branch
|
||||
*/
|
||||
TEST_F(GitCLITest, GetCurrentBranch) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create initial commit
|
||||
create_file(repo_path + "/test.txt", "initial content");
|
||||
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str());
|
||||
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str());
|
||||
|
||||
auto branch = get_current_branch(repo_path);
|
||||
ASSERT_TRUE(branch.has_value());
|
||||
// Should be either "main" or "master" depending on Git version
|
||||
EXPECT_TRUE(branch.value() == "main" || branch.value() == "master");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating a new branch
|
||||
*/
|
||||
TEST_F(GitCLITest, CreateBranch) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create initial commit
|
||||
create_file(repo_path + "/test.txt", "initial content");
|
||||
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str());
|
||||
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str());
|
||||
|
||||
// Create new branch
|
||||
GitResult result = create_branch(repo_path, "test-branch");
|
||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||
|
||||
// Verify we're on the new branch
|
||||
auto current_branch = get_current_branch(repo_path);
|
||||
ASSERT_TRUE(current_branch.has_value());
|
||||
EXPECT_EQ(current_branch.value(), "test-branch");
|
||||
|
||||
// Verify branch exists
|
||||
EXPECT_TRUE(branch_exists(repo_path, "test-branch"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding files
|
||||
*/
|
||||
TEST_F(GitCLITest, AddFiles) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create test files
|
||||
create_file(repo_path + "/file1.txt", "content1");
|
||||
create_file(repo_path + "/file2.txt", "content2");
|
||||
|
||||
// Add files
|
||||
GitResult result = add_files(repo_path, {"file1.txt", "file2.txt"});
|
||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test committing changes
|
||||
*/
|
||||
TEST_F(GitCLITest, Commit) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create and add a file
|
||||
create_file(repo_path + "/test.txt", "content");
|
||||
add_files(repo_path, {"test.txt"});
|
||||
|
||||
// Commit
|
||||
GitConfig config;
|
||||
config.user_name = "Test User";
|
||||
config.user_email = "test@example.com";
|
||||
|
||||
GitResult result = commit(repo_path, "Test commit", config);
|
||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test repository status
|
||||
*/
|
||||
TEST_F(GitCLITest, Status) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
GitResult result = status(repo_path);
|
||||
EXPECT_TRUE(result.success);
|
||||
EXPECT_FALSE(result.output.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test checkout branch
|
||||
*/
|
||||
TEST_F(GitCLITest, CheckoutBranch) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Create initial commit
|
||||
create_file(repo_path + "/test.txt", "initial content");
|
||||
system(("git -C \"" + repo_path + "\" add test.txt 2>&1 > /dev/null").c_str());
|
||||
system(("git -C \"" + repo_path + "\" commit -m \"Initial commit\" 2>&1 > /dev/null").c_str());
|
||||
|
||||
// Create and switch to new branch
|
||||
create_branch(repo_path, "test-branch");
|
||||
|
||||
// Get original branch
|
||||
auto original_branch = get_current_branch(repo_path);
|
||||
system(("git -C \"" + repo_path + "\" checkout " + original_branch.value() + " 2>&1 > /dev/null").c_str());
|
||||
|
||||
// Checkout the test branch
|
||||
GitResult result = checkout_branch(repo_path, "test-branch");
|
||||
EXPECT_TRUE(result.success) << "Error: " << result.error;
|
||||
|
||||
// Verify we're on test-branch
|
||||
auto current_branch = get_current_branch(repo_path);
|
||||
ASSERT_TRUE(current_branch.has_value());
|
||||
EXPECT_EQ(current_branch.value(), "test-branch");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test empty file list
|
||||
*/
|
||||
TEST_F(GitCLITest, AddEmptyFileList) {
|
||||
std::string repo_path = test_dir + "/test_repo";
|
||||
init_repo(repo_path);
|
||||
|
||||
// Add empty file list should succeed without error
|
||||
GitResult result = add_files(repo_path, {});
|
||||
EXPECT_TRUE(result.success);
|
||||
}
|
||||
Reference in New Issue
Block a user