From 5cca60ca9fb48e6b916e47e0ff58c30d68f6bf11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 02:33:18 +0000 Subject: [PATCH] Add Git CLI integration and enhance roadmap documentation - Implement Git CLI wrapper module (git_cli.h/cpp) - Add branch creation support to PRController - Document semantic merging approaches in Phase 2 - Document SDG analysis implementation from research - Add Bitbucket and extensible platform support docs - Update Phase 1.5 to mark Git CLI integration complete - Add comprehensive tests for Git CLI operations Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- README.md | 48 ++++- ROADMAP.md | 160 +++++++++++++++-- backend/CMakeLists.txt | 6 +- backend/README.md | 53 +++++- backend/include/wizardmerge/git/git_cli.h | 159 ++++++++++++++++ backend/src/controllers/PRController.cc | 107 ++++++++++- backend/src/git/git_cli.cpp | 209 ++++++++++++++++++++++ backend/tests/test_git_cli.cpp | 205 +++++++++++++++++++++ 8 files changed, 924 insertions(+), 23 deletions(-) create mode 100644 backend/include/wizardmerge/git/git_cli.h create mode 100644 backend/src/git/git_cli.cpp create mode 100644 backend/tests/test_git_cli.cpp diff --git a/README.md b/README.md index 9f9c251..00d60b4 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,16 @@ curl -X POST http://localhost:8080/api/pr/resolve \ "pr_url": "https://gitlab.com/owner/repo/-/merge_requests/456", "api_token": "glpat-xxx" }' + +# With branch creation (requires Git CLI) +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": "wizardmerge-resolved-pr-123" + }' ``` The API will: @@ -155,7 +165,43 @@ The API will: 3. Retrieve base and head versions of all modified files 4. Apply the three-way merge algorithm to each file 5. Auto-resolve conflicts using heuristics -6. Return merged content with conflict status +6. Optionally create a new branch with resolved changes (if `create_branch: true` and Git CLI available) +7. Return merged content with conflict status + +### Git CLI Integration + +WizardMerge includes Git CLI integration for advanced workflows: + +**Features:** +- Clone repositories locally +- Create and checkout branches +- Stage and commit resolved changes +- Push branches to remote repositories + +**Branch Creation Workflow:** + +When `create_branch: true` is set in the API request: +1. Repository is cloned to a temporary directory +2. New branch is created from the PR base branch +3. Resolved files are written to the working directory +4. Changes are staged and committed +5. Branch path is returned in the response + +**Requirements:** +- Git CLI must be installed and available in system PATH +- For pushing to remote, Git credentials must be configured (SSH keys or credential helpers) + +**Example Response with Branch Creation:** +```json +{ + "success": true, + "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", + ... +} +``` ### Authentication diff --git a/ROADMAP.md b/ROADMAP.md index a708749..9b4b51d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -77,13 +77,19 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me ### 1.5 Git Integration **Priority: MEDIUM** +- [x] **Git CLI wrapper module** (`backend/include/wizardmerge/git/git_cli.h`) + - Clone repositories + - Create and checkout branches + - Stage, commit, and push changes + - Query repository status + - Integrated into PR resolution workflow - [ ] Detect when running in Git repository - [ ] Read `.git/MERGE_HEAD` to identify conflicts - [ ] List all conflicted files - [ ] Mark files as resolved in Git - [ ] Launch from command line: `wizardmerge [file]` -**Deliverable**: `wizardmerge/git/` module and CLI enhancements +**Deliverable**: `backend/src/git/` module and CLI enhancements ✓ (Partial) --- @@ -93,18 +99,71 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me **Priority: HIGH** - [ ] Semantic merge for common file types: - - JSON: merge by key structure - - YAML: preserve hierarchy - - Package files: intelligent dependency merging - - XML: structure-aware merging + - **JSON**: Merge by key structure, preserve nested objects, handle array conflicts intelligently + - Detect structural changes vs. value changes + - Handle object key additions/deletions + - Smart array merging (by ID fields when available) + - **YAML**: Preserve hierarchy and indentation + - Maintain comments and anchors + - Detect schema-aware conflicts + - Handle multi-document YAML files + - **Package files**: Intelligent dependency merging + - `package.json` (npm): Merge dependencies by semver ranges + - `requirements.txt` (pip): Detect version conflicts + - `go.mod`, `Cargo.toml`, `pom.xml`: Language-specific dependency resolution + - Detect breaking version upgrades + - **XML**: Structure-aware merging + - Preserve DTD and schema declarations + - Match elements by attributes (e.g., `id`) + - Handle namespaces correctly - [ ] Language-aware merging (AST-based): - - Python imports and functions - - JavaScript/TypeScript modules - - Java classes and methods + - **Python**: Parse imports, function definitions, class hierarchies + - Detect import conflicts and duplicates + - Merge function/method definitions intelligently + - Handle decorators and type hints + - **JavaScript/TypeScript**: Module and export analysis + - Merge import statements without duplicates + - Handle named vs. default exports + - Detect React component conflicts + - **Java**: Class structure and method signatures + - Merge method overloads + - Handle package declarations + - Detect annotation conflicts + - **C/C++**: Header guards, include directives, function declarations + - Merge `#include` directives + - Detect macro conflicts + - Handle namespace conflicts +- [ ] SDG (System Dependence Graph) Analysis: + - **Implementation based on research paper** (docs/PAPER.md) + - Build dependency graphs at multiple levels: + - **Text-level**: Line and block dependencies + - **LLVM-IR level**: Data and control flow dependencies (for C/C++) + - **AST-level**: Semantic dependencies (for all languages) + - **Conflict Analysis**: + - Detect true conflicts vs. false conflicts + - Identify dependent code blocks affected by conflicts + - Compute conflict impact radius + - Suggest resolution based on dependency chains + - **Features**: + - 28.85% reduction in resolution time (per research) + - Suggestions for >70% of conflicted blocks + - Visual dependency graph in UI + - Highlight upstream/downstream dependencies + - **Implementation approach**: + - Use tree-sitter for AST parsing + - Integrate LLVM for IR analysis (C/C++ code) + - Build dependency database per file + - Cache analysis results for performance - [ ] Auto-resolution suggestions with confidence scores + - Assign confidence based on SDG analysis + - Learn from user's resolution patterns + - Machine learning model for conflict classification - [ ] Learn from user's resolution patterns + - Store resolution history + - Pattern matching for similar conflicts + - Suggest resolutions based on past behavior -**Deliverable**: `wizardmerge/algo/semantic/` module +**Deliverable**: `backend/src/semantic/` module with SDG analysis engine ### 2.2 Enhanced Visualization **Priority: MEDIUM** @@ -115,6 +174,10 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me - [ ] Collapsible unchanged regions - [ ] Blame/history annotations - [ ] Conflict complexity indicator +- [ ] **SDG visualization**: + - Interactive dependency graph + - Highlight conflicted nodes and their dependencies + - Show data flow and control flow edges **Deliverable**: Advanced QML components and visualization modes @@ -126,8 +189,12 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me - [ ] Show syntax errors in real-time - [ ] Auto-formatting after resolution - [ ] Import/dependency conflict detection +- [ ] **SDG-based suggestions**: + - Use LSP for real-time dependency analysis + - Validate resolution against type system + - Suggest imports/references needed -**Deliverable**: `wizardmerge/lsp/` integration module +**Deliverable**: `backend/src/lsp/` integration module ### 2.4 Multi-Frontend Architecture **Priority: HIGH** @@ -143,7 +210,76 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me **Deliverable**: `wizardmerge/core/` (backend abstraction), `frontends/qt6/` (C++/Qt6), `frontends/web/` (Next.js) -### 2.5 Collaboration Features +### 2.5 Additional Platform Support +**Priority: MEDIUM** + +- [ ] **Bitbucket** Pull Request support: + - Bitbucket Cloud API integration + - URL pattern: `https://bitbucket.org/workspace/repo/pull-requests/123` + - Authentication via App passwords or OAuth + - Support for Bitbucket Server (self-hosted) +- [ ] **Azure DevOps** Pull Request support: + - Azure DevOps REST API integration + - URL pattern: `https://dev.azure.com/org/project/_git/repo/pullrequest/123` + - Authentication via Personal Access Tokens + - Support for on-premises Azure DevOps Server +- [ ] **Gitea/Forgejo** support: + - Self-hosted Git service integration + - Compatible API with GitHub/GitLab patterns + - Community-driven platforms +- [ ] **Extensible Platform Pattern**: + - **Abstract Git Platform Interface**: + ```cpp + class GitPlatformAPI { + virtual PullRequest fetch_pr_info() = 0; + virtual std::vector fetch_file_content() = 0; + virtual bool create_comment() = 0; + virtual bool update_pr_status() = 0; + }; + ``` + - **Platform Registry**: + - Auto-detect platform from URL pattern + - Plugin system for custom platforms + - Configuration-based platform definitions + - **Common API adapter layer**: + - Normalize PR/MR data structures across platforms + - Handle authentication differences (tokens, OAuth, SSH keys) + - Abstract API versioning differences + - **Implementation Guide** (for adding new platforms): + 1. Add URL regex pattern to `parse_pr_url()` in `git_platform_client.cpp` + 2. Add platform enum value to `GitPlatform` enum + 3. Implement API client functions for the platform + 4. Add platform-specific authentication handling + 5. Add unit tests for URL parsing and API calls + 6. Update documentation with examples + - **Example: Adding Bitbucket**: + ```cpp + // 1. Add to GitPlatform enum + enum class GitPlatform { GitHub, GitLab, Bitbucket, Unknown }; + + // 2. Add URL pattern + std::regex bitbucket_regex( + R"((?:https?://)?bitbucket\.org/([^/]+)/([^/]+)/pull-requests/(\d+))" + ); + + // 3. Implement API functions + if (platform == GitPlatform::Bitbucket) { + api_url = "https://api.bitbucket.org/2.0/repositories/" + + owner + "/" + repo + "/pullrequests/" + pr_number; + // Add Bearer token authentication + headers = curl_slist_append(headers, + ("Authorization: Bearer " + token).c_str()); + } + + // 4. Map Bitbucket response to PullRequest structure + // Bitbucket uses different field names (e.g., "source" vs "head") + pr.base_ref = root["destination"]["branch"]["name"].asString(); + pr.head_ref = root["source"]["branch"]["name"].asString(); + ``` + +**Deliverable**: `backend/src/git/platform_registry.cpp` and platform-specific adapters + +### 2.6 Collaboration Features **Priority: LOW** - [ ] Add comments to conflicts @@ -154,7 +290,7 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me **Deliverable**: Collaboration UI and sharing infrastructure -### 2.6 Testing & Quality +### 2.7 Testing & Quality **Priority: HIGH** - [ ] Comprehensive test suite for merge algorithms diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index 228bf31..4de0cc2 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -14,6 +14,7 @@ find_package(CURL QUIET) # Library sources set(WIZARDMERGE_SOURCES src/merge/three_way_merge.cpp + src/git/git_cli.cpp ) # Add git sources only if CURL is available @@ -67,7 +68,10 @@ endif() if(GTest_FOUND) enable_testing() - set(TEST_SOURCES tests/test_three_way_merge.cpp) + set(TEST_SOURCES + tests/test_three_way_merge.cpp + tests/test_git_cli.cpp + ) # Add github client tests only if CURL is available if(CURL_FOUND) diff --git a/backend/README.md b/backend/README.md index 1603b21..72541e8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -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__` +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 #" +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 diff --git a/backend/include/wizardmerge/git/git_cli.h b/backend/include/wizardmerge/git/git_cli.h new file mode 100644 index 0000000..46de069 --- /dev/null +++ b/backend/include/wizardmerge/git/git_cli.h @@ -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 +#include +#include + +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& 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 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 diff --git a/backend/src/controllers/PRController.cc b/backend/src/controllers/PRController.cc index 55f09d1..e7b37c0 100644 --- a/backend/src/controllers/PRController.cc +++ b/backend/src/controllers/PRController.cc @@ -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 #include +#include using namespace wizardmerge::controllers; using namespace wizardmerge::git; @@ -180,15 +182,114 @@ 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::string temp_dir = "/tmp/wizardmerge_pr_" + std::to_string(pr_number) + "_" + + std::to_string(std::time(nullptr)); + + // 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 + auto branch_result = create_branch(temp_dir, branch_name, pr.base_ref); + + 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 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); diff --git a/backend/src/git/git_cli.cpp b/backend/src/git/git_cli.cpp new file mode 100644 index 0000000..99c36a3 --- /dev/null +++ b/backend/src/git/git_cli.cpp @@ -0,0 +1,209 @@ +/** + * @file git_cli.cpp + * @brief Implementation of Git CLI wrapper functions + */ + +#include "wizardmerge/git/git_cli.h" +#include +#include +#include +#include +#include + +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 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& 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()) { + execute_command(git_command(repo_path, + "config user.name \"" + config.user_name + "\"")); + execute_command(git_command(repo_path, + "config user.email \"" + config.user_email + "\"")); + } + + std::string cmd = "commit -m \"" + 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 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; + branch.erase(branch.find_last_not_of(" \n\r\t") + 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 diff --git a/backend/tests/test_git_cli.cpp b/backend/tests/test_git_cli.cpp new file mode 100644 index 0000000..76a600e --- /dev/null +++ b/backend/tests/test_git_cli.cpp @@ -0,0 +1,205 @@ +/** + * @file test_git_cli.cpp + * @brief Unit tests for Git CLI wrapper functionality + */ + +#include "wizardmerge/git/git_cli.h" +#include +#include +#include + +using namespace wizardmerge::git; +namespace fs = std::filesystem; + +class GitCLITest : public ::testing::Test { +protected: + std::string test_dir; + + void SetUp() override { + // Create temporary test directory + test_dir = "/tmp/wizardmerge_git_test_" + std::to_string(time(nullptr)); + 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); +}