Read about our upcoming Code of Conduct on this issue

Commit eb5bdfc1 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent eb27ae2917dc
......@@ -123,7 +123,7 @@ compile-assets pull-cache:
- .use-pg9
dependencies: ["compile-assets", "compile-assets pull-cache", "setup-test-env"]
karma:
.karma-base:
extends: .only-code-frontend-job-base
variables:
# we override the max_old_space_size to prevent OOM errors
......@@ -134,6 +134,9 @@ karma:
- scripts/gitaly-test-spawn
- date
- bundle exec rake karma
karma:
extends: .karma-base
coverage: '/^Statements *: (\d+\.\d+%)/'
artifacts:
name: coverage-javascript
......@@ -146,7 +149,12 @@ karma:
reports:
junit: junit_karma.xml
jest:
karma-foss:
extends:
- .karma-base
- .only-ee-as-if-foss
.jest-base:
extends: .only-code-frontend-job-base
script:
- scripts/gitaly-test-spawn
......@@ -154,6 +162,14 @@ jest:
- bundle exec rake frontend:fixtures
- date
- yarn jest --ci --coverage
cache:
key: jest
paths:
- tmp/jest/jest/
policy: pull-push
jest:
extends: .jest-base
artifacts:
name: coverage-frontend
expire_in: 31d
......@@ -164,11 +180,13 @@ jest:
- tmp/tests/frontend/
reports:
junit: junit_jest.xml
jest-foss:
extends:
- .jest-base
- .only-ee-as-if-foss
cache:
key: jest
paths:
- tmp/jest/jest/
policy: pull-push
policy: pull
.qa-job-base:
extends:
......
......@@ -149,3 +149,8 @@
variables:
- $CI_PROJECT_NAME == "gitlab"
- $CI_PROJECT_NAME == "gitlab-ee" # Support former project name for forks/mirrors
.only-ee-as-if-foss:
extends: .only-ee
variables:
IS_GITLAB_EE: '0'
......@@ -74,6 +74,12 @@ setup-test-env:
- .rspec-base
- .use-pg9
.rspec-base-pg9-foss:
extends:
- .rspec-base
- .use-pg9
- .only-ee-as-if-foss
.rspec-base-pg10:
extends:
- .rspec-base
......@@ -84,14 +90,27 @@ rspec unit pg9:
extends: .rspec-base-pg9
parallel: 20
rspec unit pg9-foss:
extends: .rspec-base-pg9-foss
parallel: 20
rspec integration pg9:
extends: .rspec-base-pg9
parallel: 6
rspec integration pg9-foss:
extends: .rspec-base-pg9-foss
parallel: 6
rspec system pg9:
extends: .rspec-base-pg9
parallel: 24
# TODO: This requires FOSS assets
# rspec system pg9-foss:
# extends: .rspec-base-pg9-foss
# parallel: 24
rspec unit pg10:
extends: .rspec-base-pg10
parallel: 20
......
......@@ -337,6 +337,7 @@ class GfmAutoComplete {
},
// eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${title}',
limit: 20,
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(merges) {
......
......@@ -43,7 +43,12 @@ export default {
<template>
<div class="d-flex ide-commit-editor-header align-items-center">
<file-icon :file-name="activeFile.name" :size="16" class="mr-2" />
<strong class="mr-2"> {{ activeFile.path }} </strong>
<strong class="mr-2">
<template v-if="activeFile.prevPath && activeFile.prevPath !== activeFile.path">
{{ activeFile.prevPath }} &#x2192;
</template>
{{ activeFile.path }}
</strong>
<changed-file-icon :file="activeFile" :is-centered="false" />
<div class="ml-auto">
<button
......
......@@ -110,6 +110,9 @@ export default {
>
<span class="multi-file-commit-list-file-path d-flex align-items-center">
<file-icon :file-name="file.name" class="append-right-8" />
<template v-if="file.prevName && file.prevName !== file.name">
{{ file.prevName }} &#x2192;
</template>
{{ file.name }}
</span>
<div class="ml-auto d-flex align-items-center">
......
......@@ -34,6 +34,9 @@ export default {
'getUnstagedFilesCountForPath',
'getStagedFilesCountForPath',
]),
isTree() {
return this.file.type === 'tree';
},
folderUnstagedCount() {
return this.getUnstagedFilesCountForPath(this.file.path);
},
......@@ -58,10 +61,13 @@ export default {
});
},
showTreeChangesCount() {
return this.file.type === 'tree' && this.changesCount > 0 && !this.file.opened;
return this.isTree && this.changesCount > 0 && !this.file.opened;
},
isModified() {
return this.file.changed || this.file.tempFile || this.file.staged || this.file.prevPath;
},
showChangedFileIcon() {
return this.file.changed || this.file.tempFile || this.file.staged;
return !this.isTree && this.isModified;
},
},
};
......
......@@ -30,9 +30,6 @@ export default {
showLoading() {
return !this.currentTree || this.currentTree.loading;
},
actualTreeList() {
return this.currentTree.tree.filter(entry => !entry.moved);
},
},
mounted() {
this.updateViewer(this.viewerType);
......@@ -57,9 +54,9 @@ export default {
<slot name="header"></slot>
</header>
<div class="ide-tree-body h-100">
<template v-if="actualTreeList.length">
<template v-if="currentTree.tree.length">
<file-row
v-for="file in actualTreeList"
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
......
......@@ -91,7 +91,6 @@ export default {
this.renameEntry({
path: this.entryModal.entry.path,
name: entryName,
entryPath: null,
parentPath,
}),
)
......
......@@ -155,15 +155,7 @@ export default {
this.editor.clearEditor();
this.getFileData({
path: this.file.path,
makeFileActive: false,
})
.then(() =>
this.getRawFileData({
path: this.file.path,
}),
)
this.fetchFileData()
.then(() => {
this.createEditorInstance();
})
......@@ -179,6 +171,20 @@ export default {
throw err;
});
},
fetchFileData() {
if (this.file.tempFile) {
return Promise.resolve();
}
return this.getFileData({
path: this.file.path,
makeFileActive: false,
}).then(() =>
this.getRawFileData({
path: this.file.path,
}),
);
},
createEditorInstance() {
this.editor.dispose();
......
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../stores/utils';
export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
import { decorateData, sortTree, escapeFileUrl } from '../stores/utils';
export const splitParent = path => {
const idx = path.lastIndexOf('/');
......
......@@ -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);
}
}
......
......@@ -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);
......
......@@ -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 }));
};
......@@ -154,8 +154,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
.then(() => {
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);
......
......@@ -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';
......@@ -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,
......@@ -157,9 +159,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 +216,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 +228,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