From 48d27a540511c6d0df8cb0be6762ebbc191c53ba Mon Sep 17 00:00:00 2001 From: Michael Kurz Date: Wed, 30 Nov 2022 17:12:20 +0100 Subject: [PATCH] #38 fix conflict detection and add ast parsing --- package-lock.json | 123 +++++++++++++--- package.json | 4 + .../ConflictFile/ConflictFile.js | 21 +-- .../FileDetails/FileDetails.js | 16 ++- .../FileOverview/FileOverview.js | 18 +-- .../ConflictOverview/ConflictOverview.js | 28 +--- .../team-awareness/reducers/data.js | 12 +- .../sagas/conflictCalculations.js | 131 ++++++++++-------- .../sagas/conflictDetailsOperation.js | 48 ++++++- 9 files changed, 262 insertions(+), 139 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae3cf2d6..208b853d 100755 --- a/package-lock.json +++ b/package-lock.json @@ -2180,6 +2180,39 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=" }, + "abstract-syntax-tree": { + "version": "2.20.6", + "resolved": "https://registry.npmjs.org/abstract-syntax-tree/-/abstract-syntax-tree-2.20.6.tgz", + "integrity": "sha512-3ziZ1Z8XqERIBoe6gv1G1YOqMeEA9JkmUElVzMj4BscwpYJ82SwePXPK6RU//bDZaOzX+eTvHLm1rX+yzCJdSg==", + "requires": { + "ast-types": "0.14.2", + "astring": "^1.8.1", + "esquery": "^1.4.0", + "estraverse": "^5.3.0", + "meriyah": "^4.2.1", + "source-map": "0.7.3" + }, + "dependencies": { + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "requires": { + "estraverse": "^5.1.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -2197,9 +2230,9 @@ } }, "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" }, "acorn-dynamic-import": { "version": "2.0.2", @@ -2207,6 +2240,13 @@ "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", "requires": { "acorn": "^4.0.3" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha512-fu2ygVGuMmlzG8ZeRJ0bvR41nsAkxxhbyk8bZ1SS521Z7vmgJFTQQlfz/Mp/nJexGBz+v8sC9bM6+lNgskt4Ug==" + } } }, "acorn-globals": { @@ -2215,27 +2255,24 @@ "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", "requires": { "acorn": "^4.0.4" - } - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "requires": { - "acorn": "^3.0.4" }, "dependencies": { "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha512-fu2ygVGuMmlzG8ZeRJ0bvR41nsAkxxhbyk8bZ1SS521Z7vmgJFTQQlfz/Mp/nJexGBz+v8sC9bM6+lNgskt4Ug==" } } }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" + }, "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" }, "address": { "version": "1.0.2", @@ -2800,6 +2837,21 @@ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, + "ast-types": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", + "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "requires": { + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + } + } + }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -2810,6 +2862,11 @@ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==" }, + "astring": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.8.3.tgz", + "integrity": "sha512-sRpyiNrx2dEYIMmUXprS8nlpRg2Drs8m9ElX9vVEXaCB4XEAJhKfs7IcX0IwShjuOAjLR6wzIrgoptz1n19i1A==" + }, "async": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", @@ -9051,6 +9108,21 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz", "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=" + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha512-AU7pnZkguthwBjKgCg6998ByQNIMjbuDQZ8bb78QAFZwPfmKia8AIzgY/gWgqCjnht8JLdXmB4YxA0KaV60ncQ==", + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==" + } + } } } }, @@ -13821,6 +13893,11 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==" + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" } } }, @@ -14959,6 +15036,13 @@ "whatwg-encoding": "^1.0.1", "whatwg-url": "^4.3.0", "xml-name-validator": "^2.0.1" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha512-fu2ygVGuMmlzG8ZeRJ0bvR41nsAkxxhbyk8bZ1SS521Z7vmgJFTQQlfz/Mp/nJexGBz+v8sC9bM6+lNgskt4Ug==" + } } }, "jsesc": { @@ -15855,6 +15939,11 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz", "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==" }, + "meriyah": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-4.3.3.tgz", + "integrity": "sha512-7+EKEzAp0Gvp739Dv9F6ci9FXqqAz4QDAPVaLt15s9UF9gwQLwspqQzmoGjbavnTiFXZ5hf+EDdu5MtlkMCZfA==" + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", diff --git a/package.json b/package.json index 59109ab6..6862ed46 100755 --- a/package.json +++ b/package.json @@ -66,6 +66,10 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.10.2", "@grpc/proto-loader": "^0.5.4", + "abstract-syntax-tree": "^2.20.6", + "acorn": "^8.8.1", + "acorn-jsx": "^5.3.2", + "acorn-walk": "^8.2.0", "adm-zip": "^0.4.13", "apollo-client": "^1.9.3", "apollo-client-preset": "0.1.0-1", diff --git a/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/ConflictFile/ConflictFile.js b/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/ConflictFile/ConflictFile.js index 64b69f73..d17f6699 100644 --- a/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/ConflictFile/ConflictFile.js +++ b/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/ConflictFile/ConflictFile.js @@ -9,16 +9,17 @@ export default class ConflictFile extends React.Component { } render() { - const { filePath, conflicts, displayConflictDetails, startFileConflictDetails } = this.props; - + const { filePath, conflict, displayConflictDetails, startFileConflictDetails } = this.props; const fileIcon = filePath.endsWith('.js') ? TEXT_DOCUMENT_ICON : EMPTY_DOCUMENT_ICON; const showInfoIcon = filePath.endsWith('.js'); - const showFileDetails = conflict => { - startFileConflictDetails(conflict); - displayConflictDetails(Object.assign({ overviewType: 'fileDetails' }, conflict)); + const showFileDetails = branch => { + startFileConflictDetails(Object.assign(conflict, { selectedBranch: branch })); + displayConflictDetails(Object.assign(conflict, { overviewType: 'fileDetails', selectedBranch: branch })); }; + const { branches } = conflict.files.get(conflict.selectedFile); + return (
@@ -32,20 +33,20 @@ export default class ConflictFile extends React.Component {
- Conflicts on {conflicts.length} branch{conflicts.length === 1 ? '' : 'es'} + Conflicts on {branches.length} branch{branches.length === 1 ? '' : 'es'}
- {conflicts.map(c => -
+ {branches.map(branch => +
{GIT_BRANCH_ICON}
- {c.otherStakeholder.branch} + {branch}
{showInfoIcon && - }
diff --git a/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/FileDetails/FileDetails.js b/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/FileDetails/FileDetails.js index 07310929..59cc13ed 100644 --- a/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/FileDetails/FileDetails.js +++ b/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/FileDetails/FileDetails.js @@ -9,10 +9,9 @@ export default class FileDetails extends React.Component { } render() { - const { isFileDetailsProcessing, fileDetails: { selectedConflict } } = this.props; - const { conflictStakeholder, otherStakeholder } = selectedConflict; - - console.log(this.props); + const { isFileDetailsProcessing, fileDetails: { selectedConflict, repositoryUrl } } = this.props; + const { conflictBranch, selectedBranch, selectedFile } = selectedConflict; + console.log(selectedConflict); if (isFileDetailsProcessing) { return ( @@ -27,7 +26,14 @@ export default class FileDetails extends React.Component { return (
- Conflict between branches {conflictStakeholder.branch} and {otherStakeholder.branch} + {'Conflict between branches '} + + {conflictBranch} + + {'and '} + + {selectedBranch} +
); diff --git a/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/FileOverview/FileOverview.js b/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/FileOverview/FileOverview.js index cb335335..16b0fff1 100644 --- a/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/FileOverview/FileOverview.js +++ b/ui/src/visualizations/team-awareness/components/ConflictDetailsModal/FileOverview/FileOverview.js @@ -8,32 +8,18 @@ export default class FileOverview extends React.Component { this.styles = Object.assign({}, styles); } - getUniqueFiles(selectedConflict) { - const files = new Map(); - - selectedConflict.conflicts.forEach(c => { - if (!files.has(c.file.path)) { - files.set(c.file.path, []); - } - - files.get(c.file.path).push(c); - }); - - return files; - } - render() { const { selectedConflict, displayConflictDetails, startFileConflictDetails } = this.props; const fileEntries = []; - for (const [filePath, conflicts] of this.getUniqueFiles(selectedConflict)) { + for (const [filePath, file] of selectedConflict.files) { fileEntries.push( ); } diff --git a/ui/src/visualizations/team-awareness/components/ConflictOverview/ConflictOverview.js b/ui/src/visualizations/team-awareness/components/ConflictOverview/ConflictOverview.js index c9bd20d5..cb361e3f 100644 --- a/ui/src/visualizations/team-awareness/components/ConflictOverview/ConflictOverview.js +++ b/ui/src/visualizations/team-awareness/components/ConflictOverview/ConflictOverview.js @@ -15,33 +15,8 @@ export default class ConflictOverview extends React.Component { }; } - reduceConflictsToStakeholders() { - const conflictedFiles = this.props.conflicts ? this.props.conflicts : []; - const conflicts = new Map(); - - for (const conflictedFile of conflictedFiles) { - for (const branch of conflictedFile.data) { - for (const conflict of branch.conflicts) { - const participants = `${conflict.conflictStakeholder.id}${conflict.otherStakeholder.id}`; - if (conflict.conflictStakeholder.id === conflict.otherStakeholder.id) continue; - if (!conflicts.has(participants)) { - conflicts.set(participants, { - conflictStakeholder: conflict.conflictStakeholder, - otherStakeholder: conflict.otherStakeholder, - conflicts: [conflict], - file: conflictedFile - }); - } else { - conflicts.get(participants).conflicts.push(conflict); - } - } - } - } - return Array.from(conflicts.values()); - } - render() { - const conflicts = this.reduceConflictsToStakeholders(); + const { conflicts } = this.props; const participantsTag = conflicts.length > 1 ? 'participants' : 'participant'; const handleContentMouseOver = () => { @@ -72,7 +47,6 @@ export default class ConflictOverview extends React.Component {
); } - if (conflicts.length === 0) { return (
diff --git a/ui/src/visualizations/team-awareness/reducers/data.js b/ui/src/visualizations/team-awareness/reducers/data.js index afa3b2f8..22fde2c3 100644 --- a/ui/src/visualizations/team-awareness/reducers/data.js +++ b/ui/src/visualizations/team-awareness/reducers/data.js @@ -17,12 +17,16 @@ export default handleActions( PROCESS_TEAM_AWARENESS_DATA: (state, action) => _.assign({}, state, { data: _.assign({}, state.data, action.payload) }), PROCESS_TEAM_AWARENESS_FILE_BROWSER: (state, action) => _.assign({}, state, { data: _.assign({}, state.data, action.payload) }), START_TEAM_AWARENESS_CONFLICT_PROCESSING: state => Object.assign({}, state, { isConflictsProcessing: true }), - RECEIVE_TEAM_AWARENESS_CONFLICTS: (state, action) => - Object.assign({}, state, { isConflictsProcessing: false, data: Object.assign(state.data, action.payload) }), + RECEIVE_TEAM_AWARENESS_CONFLICTS: (state, action) => { + return Object.assign({}, state, { isConflictsProcessing: false, data: Object.assign(state.data, action.payload) }); + }, START_TEAM_AWARENESS_FILE_CONFLICT_DETAILS_PROCESSING: (state, action) => - Object.assign({}, state, { isFileDetailsProcessing: true, data: { fileDetails: { selectedConflict: action.payload } } }), + Object.assign({}, state, { + isFileDetailsProcessing: true, + data: Object.assign({}, state.data, { fileDetails: { selectedConflict: action.payload } }) + }), RECEIVE_TEAM_AWARENESS_FILE_CONFLICT_DETAILS: (state, action) => - Object.assign({}, state, { isFileDetailsProcessing: false, data: Object.assign(state.data, action.payload) }) + Object.assign({}, state, { isFileDetailsProcessing: false, data: Object.assign({}, state.data, action.payload) }) }, { data: { diff --git a/ui/src/visualizations/team-awareness/sagas/conflictCalculations.js b/ui/src/visualizations/team-awareness/sagas/conflictCalculations.js index ad2865f7..d20e41ae 100644 --- a/ui/src/visualizations/team-awareness/sagas/conflictCalculations.js +++ b/ui/src/visualizations/team-awareness/sagas/conflictCalculations.js @@ -17,10 +17,10 @@ const analyseConflictsFromAppState = async appState => { return { conflicts: null }; } - const eligibleFiles = extractEligibleFiles(data.data.files, config); - const conflicts = new Map(); + const changedFiles = extractChangedFiles(data.data.files, config); + const stakeholders = new Map(); - for (const file of eligibleFiles) { + for (const file of changedFiles) { const aggregatedChanges = aggregateChanges(await getFileHunks(file.path), config); if (!aggregatedChanges.has(config.selectedConflictBranch)) { @@ -28,41 +28,48 @@ const analyseConflictsFromAppState = async appState => { return { conflicts: [] }; } - for (const selectedContributor of aggregatedChanges.get(config.selectedConflictBranch).contributors) { - let selectedContributorConflicted = false; - - for (const possibleConflict of selectedContributor.hunks) { - for (const change of possibleConflict) { - const detectedConflicts = checkForConflicts( - change, - selectedContributor.stakeholder, - config.selectedConflictBranch, - aggregatedChanges.values(), - file - ); - - if (detectedConflicts.length > 0) { - if (!conflicts.has(file.path)) { - conflicts.set(file.path, { file: file.path, data: [] }); - } - conflicts.get(file.path).data.push(...detectedConflicts); - selectedContributorConflicted = true; - break; - } + for (const branch of aggregatedChanges.keys()) { + if (branch === config.selectedConflictBranch) continue; + const conflictingContributor = aggregatedChanges.get(config.selectedConflictBranch).contributors; + const conflictingStakeholders = checkBranchConflicts(conflictingContributor, aggregatedChanges.get(branch).contributors); + if (conflictingStakeholders.length === 0) continue; + + for (const s of conflictingStakeholders) { + const { conflictStakeholder, otherStakeholder } = s; + + const combined = `${conflictStakeholder.stakeholder.id}${otherStakeholder.stakeholder.id}`; + if (!stakeholders.has(combined)) { + stakeholders.set(combined, { + conflictStakeholder: conflictStakeholder.stakeholder, + otherStakeholder: otherStakeholder.stakeholder, + conflictBranch: config.selectedConflictBranch, + files: new Map() + }); } - if (selectedContributorConflicted === true) break; + + if (!stakeholders.get(combined).files.has(file.path)) { + stakeholders.get(combined).files.set(file.path, { file: { path: file.path, url: file.webUrl }, branches: [] }); + } + stakeholders.get(combined).files.get(file.path).branches.push(branch); } } } - return { conflicts: Array.from(conflicts.values()) }; + + return { + conflicts: Array.from(stakeholders.values()) + }; }; -const extractEligibleFiles = (files, config) => { +const extractChangedFiles = (files, config) => { + const skipFilesRegex = new RegExp(/^.*(package-lock.json|yarn.lock)$/); + const eligibleFiles = new Map(); files.forEach(f => { - for (const c of f.commits.data) { - if (c.branch.startsWith(config.selectedConflictBranch) && !eligibleFiles.has(f.path)) { - eligibleFiles.set(f.path, f); + if (skipFilesRegex.test(f.path) === false) { + for (const c of f.commits.data) { + if (c.branch.startsWith(config.selectedConflictBranch) && !eligibleFiles.has(f.path)) { + eligibleFiles.set(f.path, f); + } } } }); @@ -105,37 +112,45 @@ const aggregateChanges = (data, config) => { return hunksByBranch; }; -const checkForConflicts = (sourceChange, conflictStakeholder, conflictBranch, otherEdits, file) => { - const conflicts = new Map(); - - for (const otherEdit of otherEdits) { - if (otherEdit.branch === conflictBranch) continue; - - for (const otherContributor of otherEdit.contributors) { - let contributorConflicted = false; - - for (const otherHunk of otherContributor.hunks) { - for (const otherChange of otherHunk) { - if (sourceChange.oldStart >= otherChange.newStart) { - contributorConflicted = true; - - if (!conflicts.has(otherEdit.branch)) { - conflicts.set(otherEdit.branch, { branch: otherEdit.branch, conflicts: [] }); - } - conflicts.get(otherEdit.branch).conflicts.push({ - conflictStakeholder: Object.assign({}, conflictStakeholder, { branch: conflictBranch }), - otherStakeholder: Object.assign({}, otherContributor.stakeholder, { branch: otherEdit.branch }), - change: otherChange, - file: { path: file.path, url: file.webUrl } - }); - break; - } - } - if (contributorConflicted === true) break; +const checkBranchConflicts = (conflictBranchContributors, otherContributors) => { + const detectedBranchConflicts = []; + for (const conflictContributor of conflictBranchContributors) { + for (const otherContributor of otherContributors) { + if (checkContributorConflict(conflictContributor, otherContributor)) { + detectedBranchConflicts.push({ conflictStakeholder: conflictContributor, otherStakeholder: otherContributor }); + } + } + } + return detectedBranchConflicts; +}; + +const checkContributorConflict = (conflictContributor, otherContributor) => { + for (const conflictHunk of conflictContributor.hunks) { + for (const otherHunk of otherContributor.hunks) { + if (checkHunkConflict(conflictHunk, otherHunk)) { + return true; + } + } + } + return false; +}; + +const checkHunkConflict = (conflictHunk, otherHunk) => { + for (const conflictChanges of conflictHunk) { + for (const otherChanges of otherHunk) { + if (conflictChanges.newStart >= otherChanges.oldStart && conflictChanges.newStart <= otherChanges.oldStart + otherChanges.oldLines) { + return true; + } + + if ( + otherChanges.newStart >= conflictChanges.oldStart && + otherChanges.newStart <= conflictChanges.oldStart + conflictChanges.oldLines + ) { + return true; } } } - return Array.from(conflicts.values()); + return false; }; export { processConflictBranchSelection }; diff --git a/ui/src/visualizations/team-awareness/sagas/conflictDetailsOperation.js b/ui/src/visualizations/team-awareness/sagas/conflictDetailsOperation.js index 5bdf9676..566cd1b2 100644 --- a/ui/src/visualizations/team-awareness/sagas/conflictDetailsOperation.js +++ b/ui/src/visualizations/team-awareness/sagas/conflictDetailsOperation.js @@ -1,6 +1,11 @@ import { receiveTeamAwarenessFileConflictDetails } from './index'; import { put, select } from 'redux-saga/effects'; import { getState } from '../util/util'; +import fetch from 'isomorphic-fetch'; +import { Parser } from 'acorn'; + +const githubExpression = new RegExp('https?://github\\.com/'); +const MyParser = Parser.extend(require('acorn-jsx')()); function* processFileConflictDetails() { const appState = yield select(); @@ -10,12 +15,51 @@ function* processFileConflictDetails() { const analyseFileConflictDetails = async appState => { const { data: { data: { fileDetails } } } = getState(appState); + const { selectedConflict: { files, selectedBranch, conflictBranch, selectedFile } } = fileDetails; + const { file: file } = files.get(selectedFile); + + const blobIndex = file.url.indexOf('blob/'); + if (!githubExpression.test(file.url) || blobIndex === -1) { + console.log('No tu wien binocular url'); + return { + fileDetails: { + selectedConflict: fileDetails.selectedConflict, + fetchError: 'No Github file detected' + } + }; + } + + const repositoryUrl = file.url.substring(0, blobIndex); + const githubContentUrl = repositoryUrl.replace('github.com', 'raw.githubusercontent.com'); + + const results = await Promise.all([ + fetch(`${githubContentUrl}${conflictBranch}/${file.path}`, { mode: 'cors' }), + fetch(`${githubContentUrl}${selectedBranch}/${file.path}`, { mode: 'cors' }) + ]); + if (results.filter(response => response.status >= 400).length >= 1) { + return { + fileDetails: { + selectedConflict: fileDetails.selectedConflict, + fetchError: 'Could not fetch file details from Github' + } + }; + } + const resultData = await Promise.all(results.map(r => r.text())); + try { + const trees = resultData.map(d => MyParser.parse(d, { ecmaVersion: 2020, sourceType: 'module', locations: true })); + + fileDetails.trees = trees; + } catch (e) { + fileDetails.parsingError = e; + console.error(e); + } + // TODO: Process file details conflict - // Fetch two files from github - // Load them via AST library // Compare them // Give feedback according to the AST data + fileDetails.repositoryUrl = repositoryUrl + 'blob/'; + fileDetails.testData = resultData; return { fileDetails }; };