// Version Control System Functionality class VersionControl { constructor() { this.setupEventListeners(); } setupEventListeners() { // Branch switching document.addEventListener('htmx:afterRequest', (event) => { if (event.detail.target.id === 'branch-form-container') { this.handleBranchFormResponse(event); } }); // Listen for branch switches document.addEventListener('branch-switched', () => { this.refreshContent(); }); // Handle merge operations document.addEventListener('htmx:afterRequest', (event) => { if (event.detail.target.id === 'merge-panel') { this.handleMergeResponse(event); } }); } handleBranchFormResponse(event) { if (event.detail.successful) { // Clear the branch form container document.getElementById('branch-form-container').innerHTML = ''; // Trigger branch list refresh document.body.dispatchEvent(new CustomEvent('branch-updated')); } } handleMergeResponse(event) { if (event.detail.successful) { const mergePanel = document.getElementById('merge-panel'); if (mergePanel.innerHTML.includes('Merge Successful')) { // Trigger content refresh after successful merge setTimeout(() => { this.refreshContent(); }, 1500); } } } refreshContent() { // Reload the page to show content from new branch window.location.reload(); } // Branch operations createBranch(name, parentBranch = null) { const formData = new FormData(); formData.append('name', name); if (parentBranch) { formData.append('parent', parentBranch); } return fetch('/vcs/branches/create/', { method: 'POST', body: formData, headers: { 'X-CSRFToken': this.getCsrfToken() } }).then(response => response.json()); } switchBranch(branchName) { const formData = new FormData(); formData.append('branch', branchName); return fetch('/vcs/branches/switch/', { method: 'POST', body: formData, headers: { 'X-CSRFToken': this.getCsrfToken() } }).then(response => { if (response.ok) { document.body.dispatchEvent(new CustomEvent('branch-switched')); } return response.json(); }); } // Merge operations initiateMerge(sourceBranch, targetBranch) { const formData = new FormData(); formData.append('source', sourceBranch); formData.append('target', targetBranch); return fetch('/vcs/merge/', { method: 'POST', body: formData, headers: { 'X-CSRFToken': this.getCsrfToken() } }).then(response => response.json()); } resolveConflicts(resolutions) { return fetch('/vcs/resolve-conflicts/', { method: 'POST', body: JSON.stringify(resolutions), headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.getCsrfToken() } }).then(response => response.json()); } // History operations getHistory(branch = null) { let url = '/vcs/history/'; if (branch) { url += `?branch=${encodeURIComponent(branch)}`; } return fetch(url) .then(response => response.json()); } // Comment operations async createComment(threadId, content, parentId = null) { try { const response = await fetch('/vcs/comments/create/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.getCsrfToken() }, body: JSON.stringify({ thread_id: threadId, content: content, parent_id: parentId }) }); if (!response.ok) { throw new Error('Failed to create comment'); } const comment = await response.json(); return comment; } catch (error) { this.showError('Error creating comment: ' + error.message); throw error; } } async createCommentThread(changeId, anchor, initialComment) { try { const response = await fetch('/vcs/comments/threads/create/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.getCsrfToken() }, body: JSON.stringify({ change_id: changeId, anchor: anchor, initial_comment: initialComment }) }); if (!response.ok) { throw new Error('Failed to create comment thread'); } const thread = await response.json(); return thread; } catch (error) { this.showError('Error creating comment thread: ' + error.message); throw error; } } async resolveThread(threadId) { try { const response = await fetch(`/vcs/comments/threads/${threadId}/resolve/`, { method: 'POST', headers: { 'X-CSRFToken': this.getCsrfToken() } }); if (!response.ok) { throw new Error('Failed to resolve thread'); } return await response.json(); } catch (error) { this.showError('Error resolving thread: ' + error.message); throw error; } } async editComment(commentId, content) { try { const response = await fetch(`/vcs/comments/${commentId}/`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.getCsrfToken() }, body: JSON.stringify({ content: content }) }); if (!response.ok) { throw new Error('Failed to edit comment'); } return await response.json(); } catch (error) { this.showError('Error editing comment: ' + error.message); throw error; } } async getThreadComments(threadId) { try { const response = await fetch(`/vcs/comments/threads/${threadId}/`); if (!response.ok) { throw new Error('Failed to fetch thread comments'); } return await response.json(); } catch (error) { this.showError('Error fetching comments: ' + error.message); throw error; } } initializeCommentPanel(containerId, options = {}) { const panel = new InlineCommentPanel({ ...options, onReply: async (content, parentId) => { const comment = await this.createComment( options.threadId, content, parentId ); const thread = await this.getThreadComments(options.threadId); panel.setThread(thread); }, onResolve: async () => { await this.resolveThread(options.threadId); const thread = await this.getThreadComments(options.threadId); panel.setThread(thread); } }); panel.initialize(containerId); return panel; } // Branch locking operations async acquireLock(branchName, duration = 48, reason = "") { try { const response = await fetch('/vcs/branches/lock/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.getCsrfToken() }, body: JSON.stringify({ branch: branchName, duration: duration, reason: reason }) }); if (!response.ok) { throw new Error('Failed to acquire lock'); } const result = await response.json(); if (result.success) { this.refreshLockStatus(branchName); return true; } return false; } catch (error) { this.showError('Error acquiring lock: ' + error.message); return false; } } async releaseLock(branchName, force = false) { try { const response = await fetch('/vcs/branches/unlock/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.getCsrfToken() }, body: JSON.stringify({ branch: branchName, force: force }) }); if (!response.ok) { throw new Error('Failed to release lock'); } const result = await response.json(); if (result.success) { this.refreshLockStatus(branchName); return true; } return false; } catch (error) { this.showError('Error releasing lock: ' + error.message); return false; } } async getLockStatus(branchName) { try { const response = await fetch(`/vcs/branches/${encodeURIComponent(branchName)}/lock-status/`); if (!response.ok) { throw new Error('Failed to get lock status'); } return await response.json(); } catch (error) { this.showError('Error getting lock status: ' + error.message); return null; } } async getLockHistory(branchName, limit = 10) { try { const response = await fetch( `/vcs/branches/${encodeURIComponent(branchName)}/lock-history/?limit=${limit}` ); if (!response.ok) { throw new Error('Failed to get lock history'); } return await response.json(); } catch (error) { this.showError('Error getting lock history: ' + error.message); return []; } } async refreshLockStatus(branchName) { const lockStatus = await this.getLockStatus(branchName); if (!lockStatus) return; const statusElement = document.querySelector(`[data-branch="${branchName}"] .lock-status`); if (!statusElement) return; if (lockStatus.locked) { const expiryDate = new Date(lockStatus.expires); statusElement.className = 'lock-status locked'; statusElement.innerHTML = `
${lockStatus.user} Expires: ${expiryDate.toLocaleString()} ${lockStatus.reason ? `${lockStatus.reason}` : ''}
`; } else { statusElement.className = 'lock-status unlocked'; statusElement.innerHTML = ` Unlocked `; } // Update lock controls this.updateLockControls(branchName, lockStatus); } async updateLockControls(branchName, lockStatus) { const controlsElement = document.querySelector(`[data-branch="${branchName}"] .lock-controls`); if (!controlsElement) return; if (lockStatus.locked) { controlsElement.innerHTML = ` `; } else { controlsElement.innerHTML = ` `; } } // Utility functions getCsrfToken() { return document.querySelector('[name=csrfmiddlewaretoken]').value; } showError(message) { const errorDiv = document.createElement('div'); errorDiv.className = 'bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4'; errorDiv.innerHTML = `

${message}

`; document.querySelector('.version-control-ui').prepend(errorDiv); setTimeout(() => errorDiv.remove(), 5000); } } // Import DiffViewer component import DiffViewer from './components/diff-viewer.js'; // Initialize version control document.addEventListener('DOMContentLoaded', () => { window.versionControl = new VersionControl(); // Initialize DiffViewer if diff container exists const diffContainer = document.getElementById('diff-container'); if (diffContainer) { window.diffViewer = new DiffViewer({ renderStrategy: 'side-by-side' }); diffViewer.initialize('diff-container'); } }); // Add to VersionControl class constructor class VersionControl { constructor() { this.setupEventListeners(); this.diffViewer = null; if (document.getElementById('diff-container')) { this.diffViewer = new DiffViewer({ renderStrategy: 'side-by-side' }); this.diffViewer.initialize('diff-container'); } } // Add getDiff method to VersionControl class async getDiff(version1, version2) { const url = `/vcs/diff/?v1=${encodeURIComponent(version1)}&v2=${encodeURIComponent(version2)}`; try { const response = await fetch(url); if (!response.ok) { throw new Error('Failed to fetch diff'); } const diffData = await response.json(); if (this.diffViewer) { await this.diffViewer.render(diffData); } return diffData; } catch (error) { this.showError('Error loading diff: ' + error.message); throw error; } } // Add viewChanges method to VersionControl class async viewChanges(changeId) { try { const response = await fetch(`/vcs/changes/${changeId}/`); if (!response.ok) { throw new Error('Failed to fetch changes'); } const changeData = await response.json(); if (this.diffViewer) { await this.diffViewer.render(changeData); } else { this.showError('Diff viewer not initialized'); } } catch (error) { this.showError('Error loading changes: ' + error.message); throw error; } } // Add addComment method to VersionControl class async addComment(changeId, anchor, content) { try { const response = await fetch('/vcs/comments/add/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.getCsrfToken() }, body: JSON.stringify({ change_id: changeId, anchor: anchor, content: content }) }); if (!response.ok) { throw new Error('Failed to add comment'); } const comment = await response.json(); if (this.diffViewer) { this.diffViewer.addCommentThread(anchor, [comment]); } return comment; } catch (error) { this.showError('Error adding comment: ' + error.message); throw error; } }