summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/ide/stores
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/ide/stores')
-rw-r--r--app/assets/javascripts/ide/stores/actions.js71
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js9
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js84
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js14
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js114
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js18
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js70
9 files changed, 244 insertions, 141 deletions
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 8c0119a1fed..4e18ec58feb 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -9,6 +9,7 @@ import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants';
import service from '../services';
import router from '../ide_router';
+import eventHub from '../eventhub';
export const redirectToUrl = (self, url) => visitUrl(url);
@@ -171,8 +172,10 @@ export const setCurrentBranchId = ({ commit }, currentBranchId) => {
export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
- if (file.parentPath) {
- dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile });
+ const parent = file.parentPath && state.entries[file.parentPath];
+
+ if (parent) {
+ dispatch('updateTempFlagForEntry', { file: parent, tempFile });
}
};
@@ -199,51 +202,71 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
export const deleteEntry = ({ commit, dispatch, state }, path) => {
const entry = state.entries[path];
-
+ const { prevPath, prevName, prevParentPath } = entry;
+ const isTree = entry.type === 'tree';
+
+ if (prevPath) {
+ dispatch('renameEntry', {
+ path,
+ name: prevName,
+ parentPath: prevParentPath,
+ });
+ dispatch('deleteEntry', prevPath);
+ return;
+ }
if (state.unusedSeal) dispatch('burstUnusedSeal');
if (entry.opened) dispatch('closeFile', entry);
- if (entry.type === 'tree') {
+ if (isTree) {
entry.tree.forEach(f => dispatch('deleteEntry', f.path));
}
commit(types.DELETE_ENTRY, path);
- dispatch('stageChange', path);
+
+ // Only stage if we're not a directory or a new file
+ if (!isTree && !entry.tempFile) {
+ dispatch('stageChange', path);
+ }
dispatch('triggerFilesChange');
};
export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
-export const renameEntry = (
- { dispatch, commit, state },
- { path, name, entryPath = null, parentPath },
-) => {
- const entry = state.entries[entryPath || path];
+export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPath }) => {
+ const entry = state.entries[path];
+ const newPath = parentPath ? `${parentPath}/${name}` : name;
- commit(types.RENAME_ENTRY, { path, name, entryPath, parentPath });
+ commit(types.RENAME_ENTRY, { path, name, parentPath });
if (entry.type === 'tree') {
- const slashedParentPath = parentPath ? `${parentPath}/` : '';
- const targetEntry = entryPath ? entryPath.split('/').pop() : name;
- const newParentPath = `${slashedParentPath}${targetEntry}`;
-
- state.entries[entryPath || path].tree.forEach(f => {
+ state.entries[newPath].tree.forEach(f => {
dispatch('renameEntry', {
- path,
- name,
- entryPath: f.path,
- parentPath: newParentPath,
+ path: f.path,
+ name: f.name,
+ parentPath: newPath,
});
});
} else {
- const newPath = parentPath ? `${parentPath}/${name}` : name;
const newEntry = state.entries[newPath];
- commit(types.TOGGLE_FILE_CHANGED, { file: newEntry, changed: true });
+ const isRevert = newPath === entry.prevPath;
+ const isReset = isRevert && !newEntry.changed && !newEntry.tempFile;
+ const isInChanges = state.changedFiles
+ .concat(state.stagedFiles)
+ .some(({ key }) => key === newEntry.key);
+
+ if (isReset) {
+ commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry);
+ } else if (!isInChanges) {
+ commit(types.ADD_FILE_TO_CHANGED, newPath);
+ }
+
+ if (!newEntry.tempFile) {
+ eventHub.$emit(`editor.update.model.dispose.${entry.key}`);
+ }
- if (entry.opened) {
+ if (newEntry.opened) {
router.push(`/project${newEntry.url}`);
- commit(types.TOGGLE_FILE_OPEN, entry.path);
}
}
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 7627b6e03af..59445afc7a4 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -5,7 +5,7 @@ import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
-import { setPageTitle } from '../utils';
+import { setPageTitle, replaceFileUrl } from '../utils';
import { viewerTypes, stageKeys } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
@@ -67,7 +67,7 @@ export const getFileData = (
commit(types.TOGGLE_LOADING, { entry: file });
- const url = file.prevPath ? file.url.replace(file.path, file.prevPath) : file.url;
+ const url = file.prevPath ? replaceFileUrl(file.url, file.path, file.prevPath) : file.url;
return service
.getFileData(joinPaths(gon.relative_url_root || '', url.replace('/-/', '/')))
@@ -186,11 +186,6 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
dispatch('restoreTree', file.parentPath);
}
- if (file.movedPath) {
- commit(types.DISCARD_FILE_CHANGES, file.movedPath);
- commit(types.REMOVE_FILE_FROM_CHANGED, file.movedPath);
- }
-
commit(types.DISCARD_FILE_CHANGES, path);
commit(types.REMOVE_FILE_FROM_CHANGED, path);
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index dd8f17e4f3a..20887e7d0ac 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -92,13 +92,27 @@ export const showEmptyState = ({ commit, state }, { projectId, branchId }) => {
});
};
-export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => {
- dispatch('setCurrentBranchId', branchId);
+export const loadFile = ({ dispatch, state }, { basePath }) => {
+ if (basePath) {
+ const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
+ const treeEntryKey = Object.keys(state.entries).find(
+ key => key === path && !state.entries[key].pending,
+ );
+ const treeEntry = state.entries[treeEntryKey];
- if (getters.emptyRepo) {
- return dispatch('showEmptyState', { projectId, branchId });
+ if (treeEntry) {
+ dispatch('handleTreeEntryAction', treeEntry);
+ } else {
+ dispatch('createTempEntry', {
+ name: path,
+ type: 'blob',
+ });
+ }
}
- return dispatch('getBranchData', {
+};
+
+export const loadBranch = ({ dispatch }, { projectId, branchId }) =>
+ dispatch('getBranchData', {
projectId,
branchId,
})
@@ -107,42 +121,38 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId,
projectId,
branchId,
});
- dispatch('getFiles', {
+ return dispatch('getFiles', {
projectId,
branchId,
- })
- .then(() => {
- if (basePath) {
- const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
- const treeEntryKey = Object.keys(state.entries).find(
- key => key === path && !state.entries[key].pending,
- );
- const treeEntry = state.entries[treeEntryKey];
-
- if (treeEntry) {
- dispatch('handleTreeEntryAction', treeEntry);
- } else {
- dispatch('createTempEntry', {
- name: path,
- type: 'blob',
- });
- }
- }
- })
- .catch(
- () =>
- new Error(
- sprintf(
- __('An error occurred whilst getting files for - %{branchId}'),
- {
- branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
- },
- false,
- ),
- ),
- );
+ });
})
.catch(() => {
dispatch('showBranchNotFoundError', branchId);
+ return Promise.reject();
});
+
+export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => {
+ const currentProject = state.projects[projectId];
+ if (getters.emptyRepo) {
+ return dispatch('showEmptyState', { projectId, branchId });
+ }
+ if (!currentProject || !currentProject.branches[branchId]) {
+ dispatch('setCurrentBranchId', branchId);
+
+ return dispatch('loadBranch', { projectId, branchId })
+ .then(() => dispatch('loadFile', { basePath }))
+ .catch(
+ () =>
+ new Error(
+ sprintf(
+ __('An error occurred whilst getting files for - %{branchId}'),
+ {
+ branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
+ },
+ false,
+ ),
+ ),
+ );
+ }
+ return Promise.resolve(dispatch('loadFile', { basePath }));
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 23caf2d48ed..e89ed49318b 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -152,6 +152,12 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
branch: getters.branchName,
})
.then(() => {
+ commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
+
+ setTimeout(() => {
+ commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
+ }, 5000);
+
if (state.shouldCreateMR) {
const { currentProject } = rootGetters;
const targetBranch = getters.isCreatingNewBranch
@@ -164,14 +170,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
{ root: true },
);
}
-
- commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
-
- commit(rootTypes.CLEAR_REPLACED_FILES, null, { root: true });
-
- setTimeout(() => {
- commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
- }, 5000);
})
.then(() => {
if (rootGetters.lastOpenedFile) {
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index f021729c451..f0b4718d025 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -59,8 +59,7 @@ export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
export const STAGE_CHANGE = 'STAGE_CHANGE';
export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
-
-export const CLEAR_REPLACED_FILES = 'CLEAR_REPLACED_FILES';
+export const REMOVE_FILE_FROM_STAGED_AND_CHANGED = 'REMOVE_FILE_FROM_STAGED_AND_CHANGED';
export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
@@ -79,5 +78,6 @@ export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
export const DELETE_ENTRY = 'DELETE_ENTRY';
export const RENAME_ENTRY = 'RENAME_ENTRY';
+export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY';
export const RESTORE_TREE = 'RESTORE_TREE';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index ea125214ebb..e84e2782e46 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -5,7 +5,14 @@ import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
-import { sortTree } from './utils';
+import {
+ sortTree,
+ replaceFileUrl,
+ swapInParentTreeWithSorting,
+ updateFileCollections,
+ removeFromParentTree,
+ pathsAreEqual,
+} from './utils';
export default {
[types.SET_INITIAL_DATA](state, data) {
@@ -56,11 +63,6 @@ export default {
stagedFiles: [],
});
},
- [types.CLEAR_REPLACED_FILES](state) {
- Object.assign(state, {
- replacedFiles: [],
- });
- },
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
@@ -71,16 +73,15 @@ export default {
const entry = data.entries[key];
const foundEntry = state.entries[key];
+ // NOTE: We can't clone `entry` in any of the below assignments because
+ // we need `state.entries` and the `entry.tree` to reference the same object.
if (!foundEntry) {
Object.assign(state.entries, {
[key]: entry,
});
} else if (foundEntry.deleted) {
Object.assign(state.entries, {
- [key]: {
- ...entry,
- replaces: true,
- },
+ [key]: Object.assign(entry, { replaces: true }),
});
} else {
const tree = entry.tree.filter(
@@ -157,9 +158,14 @@ export default {
changed: Boolean(changedFile),
staged: false,
replaces: false,
- prevPath: '',
- moved: false,
lastCommitSha: lastCommit.commit.id,
+
+ prevId: undefined,
+ prevPath: undefined,
+ prevName: undefined,
+ prevUrl: undefined,
+ prevKey: undefined,
+ prevParentPath: undefined,
});
if (prevPath) {
@@ -209,7 +215,9 @@ export default {
entry.deleted = true;
- parent.tree = parent.tree.filter(f => f.path !== entry.path);
+ if (parent) {
+ parent.tree = parent.tree.filter(f => f.path !== entry.path);
+ }
if (entry.type === 'blob') {
if (tempFile) {
@@ -219,51 +227,61 @@ export default {
}
}
},
- [types.RENAME_ENTRY](state, { path, name, entryPath = null, parentPath }) {
- const oldEntry = state.entries[entryPath || path];
- const slashedParentPath = parentPath ? `${parentPath}/` : '';
- const newPath = entryPath
- ? `${slashedParentPath}${oldEntry.name}`
- : `${slashedParentPath}${name}`;
+ [types.RENAME_ENTRY](state, { path, name, parentPath }) {
+ const oldEntry = state.entries[path];
+ const newPath = parentPath ? `${parentPath}/${name}` : name;
+ const isRevert = newPath === oldEntry.prevPath;
- Vue.set(state.entries, newPath, {
+ const newUrl = replaceFileUrl(oldEntry.url, oldEntry.path, newPath);
+
+ const newKey = oldEntry.key.replace(new RegExp(oldEntry.path, 'g'), newPath);
+
+ const baseProps = {
...oldEntry,
+ name,
id: newPath,
- key: `${newPath}-${oldEntry.type}-${oldEntry.path}`,
path: newPath,
- name: entryPath ? oldEntry.name : name,
- tempFile: true,
- prevPath: oldEntry.tempFile ? null : oldEntry.path,
- url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
- tree: [],
- raw: '',
- opened: false,
- parentPath,
- });
+ url: newUrl,
+ key: newKey,
+ parentPath: parentPath || '',
+ };
- oldEntry.moved = true;
- oldEntry.movedPath = newPath;
+ const prevProps =
+ oldEntry.tempFile || isRevert
+ ? {
+ prevId: undefined,
+ prevPath: undefined,
+ prevName: undefined,
+ prevUrl: undefined,
+ prevKey: undefined,
+ prevParentPath: undefined,
+ }
+ : {
+ prevId: oldEntry.prevId || oldEntry.id,
+ prevPath: oldEntry.prevPath || oldEntry.path,
+ prevName: oldEntry.prevName || oldEntry.name,
+ prevUrl: oldEntry.prevUrl || oldEntry.url,
+ prevKey: oldEntry.prevKey || oldEntry.key,
+ prevParentPath: oldEntry.prevParentPath || oldEntry.parentPath,
+ };
- const parent = parentPath
- ? state.entries[parentPath]
- : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
- const newEntry = state.entries[newPath];
-
- parent.tree = sortTree(parent.tree.concat(newEntry));
+ Vue.set(state.entries, newPath, {
+ ...baseProps,
+ ...prevProps,
+ });
- if (newEntry.type === 'blob') {
- state.changedFiles = state.changedFiles.concat(newEntry);
+ if (pathsAreEqual(oldEntry.parentPath, parentPath)) {
+ swapInParentTreeWithSorting(state, oldEntry.key, newPath, parentPath);
+ } else {
+ removeFromParentTree(state, oldEntry.key, oldEntry.parentPath);
+ swapInParentTreeWithSorting(state, oldEntry.key, newPath, parentPath);
}
- if (oldEntry.tempFile) {
- const filterMethod = f => f.path !== oldEntry.path;
-
- state.openFiles = state.openFiles.filter(filterMethod);
- state.changedFiles = state.changedFiles.filter(filterMethod);
- parent.tree = parent.tree.filter(filterMethod);
-
- Vue.delete(state.entries, oldEntry.path);
+ if (oldEntry.type === 'blob') {
+ updateFileCollections(state, oldEntry.key, newPath);
}
+
+ Vue.delete(state.entries, oldEntry.path);
},
...projectMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 1442ea7dbfa..8caeb2d73b2 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -138,8 +138,6 @@ export default {
content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false,
deleted: false,
- moved: false,
- movedPath: '',
});
if (deleted) {
@@ -179,11 +177,6 @@ export default {
});
if (stagedFile) {
- Object.assign(state, {
- replacedFiles: state.replacedFiles.concat({
- ...stagedFile,
- }),
- });
Object.assign(stagedFile, {
...state.entries[path],
});
@@ -252,4 +245,15 @@ export default {
openFiles: state.openFiles.filter(f => f.key !== file.key),
});
},
+ [types.REMOVE_FILE_FROM_STAGED_AND_CHANGED](state, file) {
+ Object.assign(state, {
+ changedFiles: state.changedFiles.filter(f => f.key !== file.key),
+ stagedFiles: state.stagedFiles.filter(f => f.key !== file.key),
+ });
+
+ Object.assign(state.entries[file.path], {
+ changed: false,
+ staged: false,
+ });
+ },
};
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index c4da482bf0a..d400b9831a9 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -6,7 +6,6 @@ export default () => ({
currentMergeRequestId: '',
changedFiles: [],
stagedFiles: [],
- replacedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 52200ce7847..a8d8ff31afe 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -50,9 +50,7 @@ export const dataStructure = () => ({
lastOpenedAt: 0,
mrChange: null,
deleted: false,
- prevPath: '',
- movedPath: '',
- moved: false,
+ prevPath: undefined,
});
export const decorateData = entity => {
@@ -129,7 +127,7 @@ export const commitActionForFile = file => {
export const getCommitFiles = stagedFiles =>
stagedFiles.reduce((acc, file) => {
- if (file.moved || file.type === 'tree') return acc;
+ if (file.type === 'tree') return acc;
return acc.concat({
...file,
@@ -148,9 +146,9 @@ export const createCommitPayload = ({
commit_message: state.commitMessage || getters.preBuiltCommitMessage,
actions: getCommitFiles(rootState.stagedFiles).map(f => ({
action: commitActionForFile(f),
- file_path: f.moved ? f.movedPath : f.path,
- previous_path: f.prevPath === '' ? undefined : f.prevPath,
- content: f.prevPath ? null : f.content || undefined,
+ file_path: f.path,
+ previous_path: f.prevPath || undefined,
+ content: f.prevPath && !f.changed ? null : f.content || undefined,
encoding: f.base64 ? 'base64' : 'text',
last_commit_id:
newBranch || f.deleted || f.prevPath || f.replaces ? undefined : f.lastCommitSha,
@@ -213,3 +211,61 @@ export const mergeTrees = (fromTree, toTree) => {
return toTree;
};
+
+export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
+
+export const replaceFileUrl = (url, oldPath, newPath) => {
+ // Add `/-/` so that we don't accidentally replace project path
+ const result = url.replace(`/-/${escapeFileUrl(oldPath)}`, `/-/${escapeFileUrl(newPath)}`);
+
+ return result;
+};
+
+export const swapInStateArray = (state, arr, key, entryPath) =>
+ Object.assign(state, {
+ [arr]: state[arr].map(f => (f.key === key ? state.entries[entryPath] : f)),
+ });
+
+export const getEntryOrRoot = (state, path) =>
+ path ? state.entries[path] : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
+
+export const swapInParentTreeWithSorting = (state, oldKey, newPath, parentPath) => {
+ if (!newPath) {
+ return;
+ }
+
+ const parent = getEntryOrRoot(state, parentPath);
+
+ if (parent) {
+ const tree = parent.tree
+ // filter out old entry && new entry
+ .filter(({ key, path }) => key !== oldKey && path !== newPath)
+ // concat new entry
+ .concat(state.entries[newPath]);
+
+ parent.tree = sortTree(tree);
+ }
+};
+
+export const removeFromParentTree = (state, oldKey, parentPath) => {
+ const parent = getEntryOrRoot(state, parentPath);
+
+ if (parent) {
+ parent.tree = sortTree(parent.tree.filter(({ key }) => key !== oldKey));
+ }
+};
+
+export const updateFileCollections = (state, key, entryPath) => {
+ ['openFiles', 'changedFiles', 'stagedFiles'].forEach(fileCollection => {
+ swapInStateArray(state, fileCollection, key, entryPath);
+ });
+};
+
+export const cleanTrailingSlash = path => path.replace(/\/$/, '');
+
+export const pathsAreEqual = (a, b) => {
+ const cleanA = a ? cleanTrailingSlash(a) : '';
+ const cleanB = b ? cleanTrailingSlash(b) : '';
+
+ return cleanA === cleanB;
+};