Visual Git Conflict Resolver Electron (JavaScript)
👤 Sharing: AI
```javascript
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs');
let mainWindow; // Keep a global reference to the window object
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true, // Required for dialog access
}
});
mainWindow.loadFile(path.join(__dirname, 'index.html'));
// Open DevTools - Remove for PRODUCTION!
// mainWindow.webContents.openDevTools();
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.on('ready', createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
// --- IPC Handlers ---
// Open File Dialog
ipcMain.handle('open-file-dialog', async () => {
const { filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt', 'conflicted'] }, // Added .conflicted for clarity
{ name: 'All Files', extensions: ['*'] }
]
});
if (filePaths && filePaths.length > 0) {
return filePaths[0]; // Return the first file path
} else {
return null; // No file selected
}
});
// Read File Content
ipcMain.handle('read-file', async (event, filePath) => {
try {
const content = fs.readFileSync(filePath, 'utf-8');
return content;
} catch (error) {
console.error('Error reading file:', error);
return null; // Or throw an error to be handled in the renderer
}
});
// Save File Content
ipcMain.handle('save-file', async (event, { filePath, content }) => {
try {
fs.writeFileSync(filePath, content);
return true; // Indicate success
} catch (error) {
console.error('Error saving file:', error);
return false; // Indicate failure
}
});
```
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Visual Git Conflict Resolver</title>
<style>
body {
font-family: sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
margin: 0;
}
.container {
display: flex;
flex: 1;
}
.editor-container {
flex: 1;
display: flex;
flex-direction: column;
padding: 10px;
}
textarea {
flex: 1;
font-family: monospace;
font-size: 14px;
padding: 10px;
border: 1px solid #ccc;
resize: none;
}
button {
padding: 10px;
margin: 5px;
cursor: pointer;
}
.conflict-highlight {
background-color: rgba(255, 0, 0, 0.2); /* Light red */
}
#status {
padding: 5px;
text-align: center;
}
</style>
</head>
<body>
<h1>Visual Git Conflict Resolver</h1>
<div id="status">Ready</div>
<button id="open-file">Open Conflicted File</button>
<div class="container">
<div class="editor-container">
<h2>Base Version</h2>
<textarea id="base-editor" readonly placeholder="Base version will be displayed here."></textarea>
</div>
<div class="editor-container">
<h2>Your Version</h2>
<textarea id="your-editor" placeholder="Your version."></textarea>
</div>
<div class="editor-container">
<h2>Their Version</h2>
<textarea id="their-editor" placeholder="Their version."></textarea>
</div>
<div class="editor-container">
<h2>Resolved Version</h2>
<textarea id="resolved-editor" placeholder="Resolved version. Edit here."></textarea>
</div>
</div>
<button id="save-file">Save Resolved File</button>
<script>
const { ipcRenderer } = require('electron');
const openFileButton = document.getElementById('open-file');
const saveFileButton = document.getElementById('save-file');
const baseEditor = document.getElementById('base-editor');
const yourEditor = document.getElementById('your-editor');
const theirEditor = document.getElementById('their-editor');
const resolvedEditor = document.getElementById('resolved-editor');
const statusElement = document.getElementById('status');
let currentFilePath = null;
openFileButton.addEventListener('click', async () => {
const filePath = await ipcRenderer.invoke('open-file-dialog');
if (filePath) {
currentFilePath = filePath;
statusElement.textContent = `Loading file: ${filePath}`;
const content = await ipcRenderer.invoke('read-file', filePath);
if (content) {
// Simple conflict splitting - This is very basic
const conflictMarkers = /<<<<<<< HEAD\n([\s\S]*?)=======\n([\s\S]*?)>>>>>>> .*/g;
const match = conflictMarkers.exec(content); // Only handle the first conflict for simplicity
if (match) {
baseEditor.value = ""; // We aren't showing the "base"
yourEditor.value = match[1]; // Your changes between <<<<<<< HEAD and =======
theirEditor.value = match[2]; // Their changes between ======= and >>>>>>>
resolvedEditor.value = content; // Start the resolved editor with the full content.
statusElement.textContent = 'Conflict detected. Please resolve.';
// VERY BASIC highlighting (not perfect, just demonstrates the concept)
// You'd want a proper diffing library for real highlighting
// Also, this simple highlighter doesn't "unhighlight"
function highlightConflicts() {
const resolvedContent = resolvedEditor.value;
const conflictRanges = [];
let lastIndex = 0;
while((match2 = conflictMarkers.exec(resolvedContent)) != null) {
conflictRanges.push({start: match2.index, end: conflictMarkers.lastIndex});
lastIndex = conflictMarkers.lastIndex;
}
resolvedEditor.value = resolvedContent; // Restore text.
// Clear existing highlights
resolvedEditor.value = resolvedContent;
// Apply highlighting
if (conflictRanges.length > 0) {
const text = resolvedEditor.value;
let highlightedText = "";
let lastEnd = 0;
for(const range of conflictRanges) {
highlightedText += text.substring(lastEnd, range.start);
highlightedText += `<span class="conflict-highlight">${text.substring(range.start, range.end)}</span>`;
lastEnd = range.end;
}
highlightedText += text.substring(lastEnd);
resolvedEditor.innerHTML = highlightedText.replace(/\n/g, "<br/>"); // VERY basic HTML rendering.
}
}
resolvedEditor.addEventListener('input', highlightConflicts);
highlightConflicts(); // Initial highlight
} else {
//No Conflict found
yourEditor.value = content; //Display the content for editing
resolvedEditor.value = content;
statusElement.textContent = 'No conflicts found.';
}
} else {
statusElement.textContent = 'Error reading file.';
}
} else {
statusElement.textContent = 'No file selected.';
}
});
saveFileButton.addEventListener('click', async () => {
if (currentFilePath) {
statusElement.textContent = 'Saving file...';
const content = resolvedEditor.value;
const success = await ipcRenderer.invoke('save-file', { filePath: currentFilePath, content });
if (success) {
statusElement.textContent = 'File saved successfully!';
} else {
statusElement.textContent = 'Error saving file.';
}
} else {
statusElement.textContent = 'No file loaded.';
}
});
</script>
</body>
</html>
```
**Explanation:**
1. **`main.js` (Electron Main Process)**:
* **Imports:** Imports necessary modules from `electron`: `app`, `BrowserWindow`, `ipcMain`, and `dialog`. Also, imports `path` and `fs` modules for file system operations.
* **`createWindow()`:**
* Creates the main application window (`BrowserWindow`).
* Sets `nodeIntegration` and `contextIsolation` to `true` and `false` respectively. This allows the renderer process (the HTML/JavaScript in `index.html`) to use Node.js modules directly. `enableRemoteModule` is true for using dialog in render process. **Note**: These settings should be carefully considered for security. Consider using a more secure approach with contextBridge for production.
* Loads `index.html` into the window.
* Optionally opens DevTools (remove for production).
* Handles the `closed` event to clean up the `mainWindow` reference.
* **App Lifecycle Events:**
* `app.on('ready', createWindow)`: Creates the window when the Electron app is ready.
* `app.on('window-all-closed', ...)`: Quits the app when all windows are closed (except on macOS).
* `app.on('activate', ...)`: Re-creates the window if the app is activated (e.g., clicking the dock icon on macOS) and no windows are open.
* **IPC Handlers (`ipcMain`)**: These are the *bridges* between the main process and the renderer process. The renderer process sends requests, and the main process handles them.
* `'open-file-dialog'`: Opens a file dialog using `dialog.showOpenDialog`. Returns the selected file path to the renderer.
* `'read-file'`: Reads the content of a file from the file system using `fs.readFileSync`. Returns the content to the renderer. Includes error handling.
* `'save-file'`: Writes content to a file using `fs.writeFileSync`. Returns a boolean indicating success or failure. Includes error handling. The handler receive an object including the file path and content to be save.
2. **`index.html` (Electron Renderer Process)**:
* **Structure:** Basic HTML structure with a title, a status display, buttons, and textareas.
* **CSS:** Simple CSS for basic layout and styling. Includes a `conflict-highlight` class for highlighting conflicting sections.
* **JavaScript:**
* **Imports `ipcRenderer`:** Imports the `ipcRenderer` module from `electron` to communicate with the main process.
* **Gets DOM Elements:** Gets references to the buttons, textareas, and status element.
* **`currentFilePath`:** Stores the path to the currently opened file.
* **`openFileButton.addEventListener('click', ...)`:**
* Uses `ipcRenderer.invoke('open-file-dialog')` to open the file dialog (handled by the main process).
* If a file is selected, it sets `currentFilePath`, updates the status, and uses `ipcRenderer.invoke('read-file', filePath)` to read the file content.
* **Conflict Detection and Splitting (Basic):** This is the core of the conflict resolution logic. It uses a regular expression (`<<<<<<< HEAD\n([\s\S]*?)=======\n([\s\S]*?)>>>>>>> .*/g`) to find the conflict markers (`<<<<<<< HEAD`, `=======`, `>>>>>>>`). This is a *very* simplistic approach and will not handle all conflict scenarios correctly. It only handles the first conflict.
* If a conflict is found, it extracts the "your" version (between `<<<<<<< HEAD` and `=======`) and the "their" version (between `=======` and `>>>>>>>`) and populates the corresponding textareas. The resolved editor is prepopulated with the full content.
* If no conflict markers are found, it assumes it's a regular file and loads the content into all relevant text areas for editing.
* **`saveFileButton.addEventListener('click', ...)`:**
* Checks if a file is loaded.
* Updates the status.
* Uses `ipcRenderer.invoke('save-file', { filePath: currentFilePath, content: resolvedEditor.value })` to save the content of the resolved editor back to the file (handled by the main process).
* Updates the status based on the success of the save operation.
* **`highlightConflicts()`**
* Highlights conflicts detected using very basic methods (regex).
* Replaces matches on the screen with a `<span class="conflict-highlight">` element.
* This method needs to be called when a conflict is detected, and also when the user makes changes in `resolved-editor`.
**To Run the Example:**
1. **Create a Project Directory:** Create a new directory for your project.
2. **Create Files:** Inside the project directory, create the following files:
* `main.js` (copy the main process code)
* `index.html` (copy the renderer process code)
* `package.json` (see below)
3. **Create `package.json`:** Create a `package.json` file in the project directory with the following content (or customize as needed):
```json
{
"name": "visual-git-conflict-resolver",
"version": "1.0.0",
"description": "A simple visual Git conflict resolver using Electron.",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"keywords": [
"electron",
"git",
"conflict",
"resolver"
],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0"
}
}
```
*Adjust the electron version to the latest.*
4. **Install Dependencies:** Open a terminal or command prompt, navigate to the project directory, and run:
```bash
npm install
```
5. **Run the App:** Run the app using:
```bash
npm start
```
**How to Use:**
1. Click the "Open Conflicted File" button.
2. Select a text file that contains Git conflict markers (e.g., `<<<<<<<`, `=======`, `>>>>>>>`). You can create a dummy file for testing.
3. The "Your Version" and "Their Version" textareas will be populated with the conflicting sections.
4. Edit the "Resolved Version" textarea to merge the changes as you see fit. The conflicts will be highlighted while you edit it.
5. Click the "Save Resolved File" button to save the resolved file.
**Important Considerations and Improvements:**
* **Robust Conflict Detection:** The regular expression used for conflict detection is very basic. For a real-world application, you need a more robust and reliable way to parse and identify Git conflicts. Consider using a dedicated diffing library (e.g., `diff2html`, `jsdiff`) to accurately identify changes and conflicts.
* **Three-Way Merge:** This example doesn't implement a true three-way merge. A proper conflict resolver would compare the "your" and "their" versions to a common ancestor ("base" version) to help resolve conflicts more intelligently. This would involve fetching the base version from Git.
* **Visual Diffing:** Instead of just showing textareas, consider using a visual diffing component to highlight the differences between the "your" and "their" versions more clearly. Libraries like `diff2html` can help with this.
* **Conflict Markers Removal:** The `saveFile` handler should remove all conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) from the saved file *after* the user has resolved the conflict. Currently, it just saves the content as it is.
* **Git Integration:** A full-fledged conflict resolver would ideally integrate directly with Git, allowing you to:
* Fetch the base version of the file.
* Stage the resolved file.
* Mark the conflict as resolved.
* **Error Handling:** Add more robust error handling to handle cases where files cannot be read or written, or where Git operations fail.
* **User Interface:** Improve the user interface with better layout, styling, and usability features (e.g., line numbers, syntax highlighting in the editors, undo/redo).
* **Security:** For production applications, carefully consider the security implications of using `nodeIntegration: true` and `contextIsolation: false`. It's generally recommended to use a more secure approach with `contextBridge` to expose only the necessary functions to the renderer process.
* **Large Files:** Handle very large files efficiently to avoid performance issues. Consider using streaming techniques to read and write large files.
* **Testing:** Write unit and integration tests to ensure the application works correctly.
This enhanced example provides a basic foundation for a visual Git conflict resolver. To build a production-ready tool, you'll need to address the considerations and improvements listed above. The conflict detection and merging logic in particular will require significant effort and a deep understanding of Git's conflict resolution process.
👁️ Viewed: 6
Comments