Commit 86db303f authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-08-03' into 'master'

CE upstream - 2018-08-03 18:21 UTC

Closes gitlab-ce#49862

See merge request gitlab-org/gitlab-ee!6789
parents 9e821920 78e34c67
...@@ -774,7 +774,7 @@ GEM ...@@ -774,7 +774,7 @@ GEM
retriable (3.1.1) retriable (3.1.1)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (3.1.1) rouge (3.2.0)
rqrcode (0.7.0) rqrcode (0.7.0)
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
......
...@@ -783,7 +783,7 @@ GEM ...@@ -783,7 +783,7 @@ GEM
retriable (3.1.1) retriable (3.1.1)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (3.1.1) rouge (3.2.0)
rqrcode (0.7.0) rqrcode (0.7.0)
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
......
...@@ -4,6 +4,7 @@ import icon from '~/vue_shared/components/icon.vue'; ...@@ -4,6 +4,7 @@ import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue'; import newModal from './modal.vue';
import upload from './upload.vue'; import upload from './upload.vue';
import ItemButton from './button.vue'; import ItemButton from './button.vue';
import { modalTypes } from '../../constants';
export default { export default {
components: { components: {
...@@ -56,6 +57,7 @@ export default { ...@@ -56,6 +57,7 @@ export default {
this.dropdownOpen = !this.dropdownOpen; this.dropdownOpen = !this.dropdownOpen;
}, },
}, },
modalTypes,
}; };
</script> </script>
...@@ -74,7 +76,7 @@ export default { ...@@ -74,7 +76,7 @@ export default {
@click.stop="openDropdown()" @click.stop="openDropdown()"
> >
<icon <icon
name="hamburger" name="ellipsis_v"
/> />
<icon <icon
name="arrow-down" name="arrow-down"
...@@ -106,11 +108,20 @@ export default { ...@@ -106,11 +108,20 @@ export default {
class="d-flex" class="d-flex"
icon="folder-new" icon="folder-new"
icon-classes="mr-2" icon-classes="mr-2"
@click="createNewItem('tree')" @click="createNewItem($options.modalTypes.tree)"
/> />
</li> </li>
<li class="divider"></li> <li class="divider"></li>
</template> </template>
<li>
<item-button
:label="__('Rename')"
class="d-flex"
icon="pencil"
icon-classes="mr-2"
@click="createNewItem($options.modalTypes.rename)"
/>
</li>
<li> <li>
<item-button <item-button
:label="__('Delete')" :label="__('Delete')"
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { __ } from '~/locale'; import { __ } from '~/locale';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue'; import GlModal from '~/vue_shared/components/gl_modal.vue';
import { modalTypes } from '../../constants';
export default { export default {
components: { components: {
...@@ -13,42 +14,58 @@ export default { ...@@ -13,42 +14,58 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['newEntryModal']), ...mapState(['entryModal']),
entryName: { entryName: {
get() { get() {
return this.name || (this.newEntryModal.path !== '' ? `${this.newEntryModal.path}/` : ''); if (this.entryModal.type === modalTypes.rename) {
return this.name || this.entryModal.entry.name;
}
return this.name || (this.entryModal.path !== '' ? `${this.entryModal.path}/` : '');
}, },
set(val) { set(val) {
this.name = val; this.name = val;
}, },
}, },
modalTitle() { modalTitle() {
if (this.newEntryModal.type === 'tree') { if (this.entryModal.type === modalTypes.tree) {
return __('Create new directory'); return __('Create new directory');
} else if (this.entryModal.type === modalTypes.rename) {
return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
} }
return __('Create new file'); return __('Create new file');
}, },
buttonLabel() { buttonLabel() {
if (this.newEntryModal.type === 'tree') { if (this.entryModal.type === modalTypes.tree) {
return __('Create directory'); return __('Create directory');
} else if (this.entryModal.type === modalTypes.rename) {
return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
} }
return __('Create file'); return __('Create file');
}, },
}, },
methods: { methods: {
...mapActions(['createTempEntry']), ...mapActions(['createTempEntry', 'renameEntry']),
createEntryInStore() { submitForm() {
this.createTempEntry({ if (this.entryModal.type === modalTypes.rename) {
name: this.name, this.renameEntry({
type: this.newEntryModal.type, path: this.entryModal.entry.path,
}); name: this.entryName,
});
} else {
this.createTempEntry({
name: this.name,
type: this.entryModal.type,
});
}
}, },
focusInput() { focusInput() {
setTimeout(() => { this.$refs.fieldName.focus();
this.$refs.fieldName.focus(); },
}); closedModal() {
this.name = '';
}, },
}, },
}; };
...@@ -60,8 +77,9 @@ export default { ...@@ -60,8 +77,9 @@ export default {
:header-title-text="modalTitle" :header-title-text="modalTitle"
:footer-primary-button-text="buttonLabel" :footer-primary-button-text="buttonLabel"
footer-primary-button-variant="success" footer-primary-button-variant="success"
@submit="createEntryInStore" @submit="submitForm"
@open="focusInput" @open="focusInput"
@closed="closedModal"
> >
<div <div
class="form-group row" class="form-group row"
......
...@@ -134,8 +134,7 @@ export default { ...@@ -134,8 +134,7 @@ export default {
.replace(/[/]$/g, ''); .replace(/[/]$/g, '');
// - strip ending "/" // - strip ending "/"
const filePath = this.file.path const filePath = this.file.path.replace(/[/]$/g, '');
.replace(/[/]$/g, '');
return filePath === routePath; return filePath === routePath;
}, },
...@@ -194,7 +193,7 @@ export default { ...@@ -194,7 +193,7 @@ export default {
data-container="body" data-container="body"
data-placement="right" data-placement="right"
name="file-modified" name="file-modified"
css-classes="prepend-left-5 multi-file-modified" css-classes="prepend-left-5 ide-file-modified"
/> />
</span> </span>
<changed-file-icon <changed-file-icon
...@@ -208,7 +207,6 @@ export default { ...@@ -208,7 +207,6 @@ export default {
</span> </span>
<new-dropdown <new-dropdown
:type="file.type" :type="file.type"
:branch="file.branchId"
:path="file.path" :path="file.path"
:mouse-over="mouseOver" :mouse-over="mouseOver"
class="float-right prepend-left-8" class="float-right prepend-left-8"
......
...@@ -53,3 +53,8 @@ export const commitItemIconMap = { ...@@ -53,3 +53,8 @@ export const commitItemIconMap = {
class: 'ide-file-deletion', class: 'ide-file-deletion',
}, },
}; };
export const modalTypes = {
rename: 'rename',
tree: 'tree',
};
...@@ -8,7 +8,7 @@ export default { ...@@ -8,7 +8,7 @@ export default {
}); });
}, },
getRawFileData(file) { getRawFileData(file) {
if (file.tempFile) { if (file.tempFile && !file.prevPath) {
return Promise.resolve(file.content); return Promise.resolve(file.content);
} }
......
...@@ -186,13 +186,39 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => { ...@@ -186,13 +186,39 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
}; };
export const deleteEntry = ({ commit, dispatch, state }, path) => { export const deleteEntry = ({ commit, dispatch, state }, path) => {
dispatch('burstUnusedSeal'); const entry = state.entries[path];
dispatch('closeFile', state.entries[path]);
if (state.unusedSeal) dispatch('burstUnusedSeal');
if (entry.opened) dispatch('closeFile', entry);
if (entry.type === 'tree') {
entry.tree.forEach(f => dispatch('deleteEntry', f.path));
}
commit(types.DELETE_ENTRY, path); commit(types.DELETE_ENTRY, path);
if (entry.parentPath && state.entries[entry.parentPath].tree.length === 0) {
dispatch('deleteEntry', entry.parentPath);
}
}; };
export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => {
const entry = state.entries[entryPath || path];
commit(types.RENAME_ENTRY, { path, name, entryPath });
if (entry.type === 'tree') {
state.entries[entryPath || path].tree.forEach(f =>
dispatch('renameEntry', { path, name, entryPath: f.path }),
);
}
if (!entryPath) {
dispatch('deleteEntry', path);
}
};
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
......
...@@ -62,14 +62,14 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { ...@@ -62,14 +62,14 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => { export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
const file = state.entries[path]; const file = state.entries[path];
if (file.raw || file.tempFile) return Promise.resolve(); if (file.raw || (file.tempFile && !file.prevPath)) return Promise.resolve();
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
const url = file.prevPath ? file.url.replace(file.path, file.prevPath) : file.url;
return service return service
.getFileData( .getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${url.replace('/-/', '/')}`)
`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
)
.then(({ data, headers }) => { .then(({ data, headers }) => {
const normalizedHeaders = normalizeHeaders(headers); const normalizedHeaders = normalizeHeaders(headers);
setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE'])); setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
...@@ -101,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = ...@@ -101,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
service service
.getRawFileData(file) .getRawFileData(file)
.then(raw => { .then(raw => {
if (!file.tempFile) commit(types.SET_FILE_RAW_DATA, { file, raw }); if (!(file.tempFile && !file.prevPath)) commit(types.SET_FILE_RAW_DATA, { file, raw });
if (file.mrChange && file.mrChange.new_file === false) { if (file.mrChange && file.mrChange.new_file === false) {
service service
.getBaseRawFileData(file, baseSha) .getBaseRawFileData(file, baseSha)
...@@ -176,9 +176,22 @@ export const setFileViewMode = ({ commit }, { file, viewMode }) => { ...@@ -176,9 +176,22 @@ export const setFileViewMode = ({ commit }, { file, viewMode }) => {
export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => { export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => {
const file = state.entries[path]; const file = state.entries[path];
if (file.deleted && file.parentPath) {
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.DISCARD_FILE_CHANGES, path);
commit(types.REMOVE_FILE_FROM_CHANGED, path); commit(types.REMOVE_FILE_FROM_CHANGED, path);
if (file.prevPath) {
dispatch('discardFileChanges', file.prevPath);
}
if (file.tempFile && file.opened) { if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, path); commit(types.TOGGLE_FILE_OPEN, path);
} else if (getters.activeFile && file.path === getters.activeFile.path) { } else if (getters.activeFile && file.path === getters.activeFile.path) {
......
...@@ -89,3 +89,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = ...@@ -89,3 +89,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
resolve(); resolve();
} }
}); });
export const restoreTree = ({ dispatch, commit, state }, path) => {
const entry = state.entries[path];
commit(types.RESTORE_TREE, path);
if (entry.parentPath) {
dispatch('restoreTree', entry.parentPath);
}
};
...@@ -67,9 +67,9 @@ export const someUncommitedChanges = state => ...@@ -67,9 +67,9 @@ export const someUncommitedChanges = state =>
!!(state.changedFiles.length || state.stagedFiles.length); !!(state.changedFiles.length || state.stagedFiles.length);
export const getChangesInFolder = state => path => { export const getChangesInFolder = state => path => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length; const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f.path, path)).length;
const stagedFilesCount = state.stagedFiles.filter( const stagedFilesCount = state.stagedFiles.filter(
f => filePathMatches(f, path) && !getChangedFile(state)(f.path), f => filePathMatches(f.path, path) && !getChangedFile(state)(f.path),
).length; ).length;
return changedFilesCount + stagedFilesCount; return changedFilesCount + stagedFilesCount;
......
...@@ -77,3 +77,6 @@ export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; ...@@ -77,3 +77,6 @@ export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
export const DELETE_ENTRY = 'DELETE_ENTRY'; export const DELETE_ENTRY = 'DELETE_ENTRY';
export const RENAME_ENTRY = 'RENAME_ENTRY';
export const RESTORE_TREE = 'RESTORE_TREE';
...@@ -131,11 +131,14 @@ export default { ...@@ -131,11 +131,14 @@ export default {
}, },
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) { [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
const changedFile = state.changedFiles.find(f => f.path === file.path); const changedFile = state.changedFiles.find(f => f.path === file.path);
const { prevPath } = file;
Object.assign(state.entries[file.path], { Object.assign(state.entries[file.path], {
raw: file.content, raw: file.content,
changed: !!changedFile, changed: !!changedFile,
staged: false, staged: false,
prevPath: '',
moved: false,
lastCommit: Object.assign(state.entries[file.path].lastCommit, { lastCommit: Object.assign(state.entries[file.path].lastCommit, {
id: lastCommit.commit.id, id: lastCommit.commit.id,
url: lastCommit.commit_path, url: lastCommit.commit_path,
...@@ -144,6 +147,18 @@ export default { ...@@ -144,6 +147,18 @@ export default {
updatedAt: lastCommit.commit.authored_date, updatedAt: lastCommit.commit.authored_date,
}), }),
}); });
if (prevPath) {
// Update URLs after file has moved
const regex = new RegExp(`${prevPath}$`);
Object.assign(state.entries[file.path], {
rawPath: file.rawPath.replace(regex, file.path),
permalink: file.permalink.replace(regex, file.path),
commitsPath: file.commitsPath.replace(regex, file.path),
blamePath: file.blamePath.replace(regex, file.path),
});
}
}, },
[types.BURST_UNUSED_SEAL](state) { [types.BURST_UNUSED_SEAL](state) {
Object.assign(state, { Object.assign(state, {
...@@ -169,7 +184,11 @@ export default { ...@@ -169,7 +184,11 @@ export default {
}, },
[types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) { [types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) {
Object.assign(state, { Object.assign(state, {
newEntryModal: { type, path }, entryModal: {
type,
path,
entry: { ...state.entries[path] },
},
}); });
}, },
[types.DELETE_ENTRY](state, path) { [types.DELETE_ENTRY](state, path) {
...@@ -179,8 +198,48 @@ export default { ...@@ -179,8 +198,48 @@ export default {
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
entry.deleted = true; entry.deleted = true;
state.changedFiles = state.changedFiles.concat(entry);
parent.tree = parent.tree.filter(f => f.path !== entry.path); parent.tree = parent.tree.filter(f => f.path !== entry.path);
if (entry.type === 'blob') {
state.changedFiles = state.changedFiles.concat(entry);
}
},
[types.RENAME_ENTRY](state, { path, name, entryPath = null }) {
const oldEntry = state.entries[entryPath || path];
const nameRegex =
!entryPath && oldEntry.type === 'blob'
? new RegExp(`${oldEntry.name}$`)
: new RegExp(`^${path}`);
const newPath = oldEntry.path.replace(nameRegex, name);
const parentPath = oldEntry.parentPath ? oldEntry.parentPath.replace(nameRegex, name) : '';
state.entries[newPath] = {
...oldEntry,
id: newPath,
key: `${name}-${oldEntry.type}-${oldEntry.id}`,
path: newPath,
name: entryPath ? oldEntry.name : name,
tempFile: true,
prevPath: oldEntry.path,
url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
tree: [],
parentPath,
raw: '',
};
oldEntry.moved = true;
oldEntry.movedPath = newPath;
const parent = parentPath
? state.entries[parentPath]
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
const newEntry = state.entries[newPath];
parent.tree = sortTree(parent.tree.concat(newEntry));
if (newEntry.type === 'blob') {
state.changedFiles = state.changedFiles.concat(newEntry);
}
}, },
...projectMutations, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
......
...@@ -53,15 +53,25 @@ export default { ...@@ -53,15 +53,25 @@ export default {
}, },
[types.SET_FILE_RAW_DATA](state, { file, raw }) { [types.SET_FILE_RAW_DATA](state, { file, raw }) {
const openPendingFile = state.openFiles.find( const openPendingFile = state.openFiles.find(
f => f.path === file.path && f.pending && !f.tempFile, f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath),
); );
Object.assign(state.entries[file.path], { if (file.tempFile) {
raw, Object.assign(state.entries[file.path], {
}); content: raw,
});
} else {
Object.assign(state.entries[file.path], {
raw,
});
}
if (openPendingFile) { if (!openPendingFile) return;
if (!openPendingFile.tempFile) {
openPendingFile.raw = raw; openPendingFile.raw = raw;
} else if (openPendingFile.tempFile) {
openPendingFile.content = raw;
} }
}, },
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) { [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
...@@ -119,12 +129,14 @@ export default { ...@@ -119,12 +129,14 @@ export default {
[types.DISCARD_FILE_CHANGES](state, path) { [types.DISCARD_FILE_CHANGES](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
const entry = state.entries[path]; const entry = state.entries[path];
const { deleted } = entry; const { deleted, prevPath } = entry;
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
content: stagedFile ? stagedFile.content : state.entries[path].raw, content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false, changed: false,
deleted: false, deleted: false,
moved: false,
movedPath: '',
}); });
if (deleted) { if (deleted) {
...@@ -133,6 +145,12 @@ export default { ...@@ -133,6 +145,12 @@ export default {
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
parent.tree = sortTree(parent.tree.concat(entry)); parent.tree = sortTree(parent.tree.concat(entry));
} else if (prevPath) {
const parent = entry.parentPath
? state.entries[entry.parentPath]
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
parent.tree = parent.tree.filter(f => f.path !== path);
} }
}, },
[types.ADD_FILE_TO_CHANGED](state, path) { [types.ADD_FILE_TO_CHANGED](state, path) {
......
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { sortTree } from '../utils';
export default { export default {
[types.TOGGLE_TREE_OPEN](state, path) { [types.TOGGLE_TREE_OPEN](state, path) {
...@@ -36,4 +37,14 @@ export default { ...@@ -36,4 +37,14 @@ export default {
changedFiles: [], changedFiles: [],
}); });
}, },
[types.RESTORE_TREE](state, path) {
const entry = state.entries[path];
const parent = entry.parentPath
? state.entries[entry.parentPath]
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
if (!parent.tree.find(f => f.path === path)) {
parent.tree = sortTree(parent.tree.concat(entry));
}
},
}; };
...@@ -26,8 +26,9 @@ export default () => ({ ...@@ -26,8 +26,9 @@ export default () => ({
rightPane: null, rightPane: null,
links: {}, links: {},
errorMessage: null, errorMessage: null,
newEntryModal: { entryModal: {
type: '', type: '',
path: '', path: '',
entry: {},
}, },
}); });
...@@ -47,6 +47,9 @@ export const dataStructure = () => ({ ...@@ -47,6 +47,9 @@ export const dataStructure = () => ({
lastOpenedAt: 0, lastOpenedAt: 0,
mrChange: null, mrChange: null,
deleted: false, deleted: false,
prevPath: '',
movedPath: '',
moved: false,
}); });
export const decorateData = entity => { export const decorateData = entity => {
...@@ -107,7 +110,9 @@ export const setPageTitle = title => { ...@@ -107,7 +110,9 @@ export const setPageTitle = title => {
}; };
export const commitActionForFile = file => { export const commitActionForFile = file => {
if (file.deleted) { if (file.prevPath) {
return 'move';
} else if (file.deleted) {
return 'delete'; return 'delete';
} else if (file.tempFile) { } else if (file.tempFile) {
return 'create'; return 'create';
...@@ -116,15 +121,12 @@ export const commitActionForFile = file => { ...@@ -116,15 +121,12 @@ export const commitActionForFile = file => {
return 'update'; return 'update';
}; };
export const getCommitFiles = (stagedFiles, deleteTree = false) => export const getCommitFiles = stagedFiles =>
stagedFiles.reduce((acc, file) => { stagedFiles.reduce((acc, file) => {
if ((file.deleted || deleteTree) && file.type === 'tree') { if (file.moved) return acc;
return acc.concat(getCommitFiles(file.tree, true));
}
return acc.concat({ return acc.concat({
...file, ...file,
deleted: deleteTree || file.deleted,
}); });
}, []); }, []);
...@@ -134,9 +136,10 @@ export const createCommitPayload = ({ branch, getters, newBranch, state, rootSta ...@@ -134,9 +136,10 @@ export const createCommitPayload = ({ branch, getters, newBranch, state, rootSta
actions: getCommitFiles(rootState.stagedFiles).map(f => ({ actions: getCommitFiles(rootState.stagedFiles).map(f => ({
action: commitActionForFile(f), action: commitActionForFile(f),
file_path: f.path, file_path: f.path,
content: f.content, previous_path: f.prevPath === '' ? undefined : f.prevPath,
content: f.content || undefined,
encoding: f.base64 ? 'base64' : 'text', encoding: f.base64 ? 'base64' : 'text',
last_commit_id: newBranch || f.deleted ? undefined : f.lastCommitSha, last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha,
})), })),
start_branch: newBranch ? rootState.currentBranchId : undefined, start_branch: newBranch ? rootState.currentBranchId : undefined,
}); });
...@@ -164,8 +167,7 @@ export const sortTree = sortedTree => ...@@ -164,8 +167,7 @@ export const sortTree = sortedTree =>
) )
.sort(sortTreesByTypeAndName); .sort(sortTreesByTypeAndName);
export const filePathMatches = (f, path) => export const filePathMatches = (filePath, path) => filePath.indexOf(`${path}/`) === 0;
f.path.replace(new RegExp(`${f.name}$`), '').indexOf(`${path}/`) === 0;
export const getChangesCountForFiles = (files, path) => export const getChangesCountForFiles = (files, path) =>
files.filter(f => filePathMatches(f, path)).length; files.filter(f => filePathMatches(f.path, path)).length;
<script> <script>
import $ from 'jquery';
const buttonVariants = ['danger', 'primary', 'success', 'warning']; const buttonVariants = ['danger', 'primary', 'success', 'warning'];
const sizeVariants = ['sm', 'md', 'lg', 'xl']; const sizeVariants = ['sm', 'md', 'lg', 'xl'];
...@@ -38,6 +40,12 @@ export default { ...@@ -38,6 +40,12 @@ export default {
return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`; return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`;
}, },
}, },
mounted() {
$(this.$el).on('shown.bs.modal', this.opened).on('hidden.bs.modal', this.closed);
},
beforeDestroy() {
$(this.$el).off('shown.bs.modal', this.opened).off('hidden.bs.modal', this.closed);
},
methods: { methods: {
emitCancel(event) { emitCancel(event) {
this.$emit('cancel', event); this.$emit('cancel', event);
...@@ -45,10 +53,11 @@ export default { ...@@ -45,10 +53,11 @@ export default {
emitSubmit(event) { emitSubmit(event) {
this.$emit('submit', event); this.$emit('submit', event);
}, },
opened({ propertyName }) { opened() {
if (propertyName === 'opacity') { this.$emit('open');
this.$emit('open'); },
} closed() {
this.$emit('closed');
}, },
}, },
}; };
...@@ -60,7 +69,6 @@ export default { ...@@ -60,7 +69,6 @@ export default {
class="modal fade" class="modal fade"
tabindex="-1" tabindex="-1"
role="dialog" role="dialog"
@transitionend="opened"
> >
<div <div
:class="modalSizeClass" :class="modalSizeClass"
......
...@@ -72,6 +72,15 @@ export default { ...@@ -72,6 +72,15 @@ export default {
is-new is-new
/> />
<issues-block
v-if="newIssues.length"
:component="component"
:issues="newIssues"
class="js-mr-code-new-issues"
status="failed"
is-new
/>
<issues-block <issues-block
v-if="unresolvedIssues.length" v-if="unresolvedIssues.length"
:component="component" :component="component"
......
...@@ -1377,6 +1377,7 @@ ...@@ -1377,6 +1377,7 @@
.ide-entry-dropdown-toggle { .ide-entry-dropdown-toggle {
padding: $gl-padding-4; padding: $gl-padding-4;
color: $gl-text-color;
background-color: $theme-gray-100; background-color: $theme-gray-100;
&:hover { &:hover {
...@@ -1389,6 +1390,10 @@ ...@@ -1389,6 +1390,10 @@
background-color: $blue-500; background-color: $blue-500;
outline: 0; outline: 0;
} }
svg {
fill: currentColor;
}
} }
.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle { .ide-new-btn .dropdown.show .ide-entry-dropdown-toggle {
......
...@@ -109,24 +109,6 @@ ...@@ -109,24 +109,6 @@
line-height: 21px; line-height: 21px;
} }
.last-commit {
@include str-truncated(506px);
.fa-angle-right {
margin-left: 5px;
}
@include media-breakpoint-only(md) {
@include str-truncated(450px);
}
}
.commit-history-link-spacer {
margin: 0 10px;
color: $white-normal;
}
&:hover:not(.tree-truncated-warning) { &:hover:not(.tree-truncated-warning) {
td { td {
background-color: $row-hover; background-color: $row-hover;
......
...@@ -108,6 +108,7 @@ class ApplicationController < ActionController::Base ...@@ -108,6 +108,7 @@ class ApplicationController < ActionController::Base
def append_info_to_payload(payload) def append_info_to_payload(payload)
super super
payload[:remote_ip] = request.remote_ip payload[:remote_ip] = request.remote_ip
logged_user = auth_user logged_user = auth_user
...@@ -122,12 +123,16 @@ class ApplicationController < ActionController::Base ...@@ -122,12 +123,16 @@ class ApplicationController < ActionController::Base
end end
end end
##
# Controllers such as GitHttpController may use alternative methods # Controllers such as GitHttpController may use alternative methods
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user # (e.g. tokens) to authenticate the user, whereas Devise sets current_user.
#
def auth_user def auth_user
return current_user if current_user.present? if user_signed_in?
current_user
return try(:authenticated_user) else
try(:authenticated_user)
end
end end
# This filter handles personal access tokens, and atom requests with rss tokens # This filter handles personal access tokens, and atom requests with rss tokens
......
# frozen_string_literal: true
class ApplicationSetting class ApplicationSetting
class Term < ActiveRecord::Base class Term < ActiveRecord::Base
include CacheMarkdownField include CacheMarkdownField
......
# frozen_string_literal: true
class GroupBadge < Badge class GroupBadge < Badge
belongs_to :group belongs_to :group
......
# frozen_string_literal: true
class ProjectBadge < Badge class ProjectBadge < Badge
belongs_to :project belongs_to :project
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
module Auxiliary module Auxiliary
extend ActiveSupport::Concern extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Balsamiq < Base class Balsamiq < Base
include Rich include Rich
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Base class Base
PARTIAL_PATH_PREFIX = 'projects/blob/viewers'.freeze PARTIAL_PATH_PREFIX = 'projects/blob/viewers'.freeze
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class BinarySTL < Base class BinarySTL < Base
include Rich include Rich
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Cartfile < DependencyManager class Cartfile < DependencyManager
include Static include Static
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Changelog < Base class Changelog < Base
include Auxiliary include Auxiliary
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
module ClientSide module ClientSide
extend ActiveSupport::Concern extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class ComposerJson < DependencyManager class ComposerJson < DependencyManager
include ServerSide include ServerSide
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Contributing < Base class Contributing < Base
include Auxiliary include Auxiliary
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class DependencyManager < Base class DependencyManager < Base
include Auxiliary include Auxiliary
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Download < Base class Download < Base
include Simple include Simple
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Empty < Base class Empty < Base
include Simple include Simple
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Gemfile < DependencyManager class Gemfile < DependencyManager
include Static include Static
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Gemspec < DependencyManager class Gemspec < DependencyManager
include ServerSide include ServerSide
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class GitlabCiYml < Base class GitlabCiYml < Base
include ServerSide include ServerSide
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class GodepsJson < DependencyManager class GodepsJson < DependencyManager
include Static include Static
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Image < Base class Image < Base
include Rich include Rich
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class License < Base class License < Base
include Auxiliary include Auxiliary
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Markup < Base class Markup < Base
include Rich include Rich
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Notebook < Base class Notebook < Base
include Rich include Rich
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class PackageJson < DependencyManager class PackageJson < DependencyManager
include ServerSide include ServerSide
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class PDF < Base class PDF < Base
include Rich include Rich
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Podfile < DependencyManager class Podfile < DependencyManager
include Static include Static
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Podspec < DependencyManager class Podspec < DependencyManager
include ServerSide include ServerSide
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class PodspecJson < Podspec class PodspecJson < Podspec
self.file_types = %i(podspec_json) self.file_types = %i(podspec_json)
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Readme < Base class Readme < Base
include Auxiliary include Auxiliary
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class RequirementsTxt < DependencyManager class RequirementsTxt < DependencyManager
include Static include Static
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
module Rich module Rich
extend ActiveSupport::Concern extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class RouteMap < Base class RouteMap < Base
include ServerSide include ServerSide
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
module ServerSide module ServerSide
extend ActiveSupport::Concern extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
module Simple module Simple
extend ActiveSupport::Concern extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Sketch < Base class Sketch < Base
include Rich include Rich
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
module Static module Static
extend ActiveSupport::Concern extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class SVG < Base class SVG < Base
include Rich include Rich
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Text < Base class Text < Base
include Simple include Simple
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class TextSTL < BinarySTL class TextSTL < BinarySTL
self.binary = false self.binary = false
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class Video < Base class Video < Base
include Rich include Rich
......
# frozen_string_literal: true
module BlobViewer module BlobViewer
class YarnLock < DependencyManager class YarnLock < DependencyManager
include Static include Static
......
# frozen_string_literal: true
module Ci module Ci
class ArtifactBlob class ArtifactBlob
include BlobLike include BlobLike
......
# frozen_string_literal: true
module Ci module Ci
class Build < CommitStatus class Build < CommitStatus
prepend ArtifactMigratable prepend ArtifactMigratable
......
# frozen_string_literal: true
module Ci module Ci
# The purpose of this class is to store Build related data that can be disposed. # The purpose of this class is to store Build related data that can be disposed.
# Data that should be persisted forever, should be stored with Ci::Build model. # Data that should be persisted forever, should be stored with Ci::Build model.
......
# frozen_string_literal: true
module Ci module Ci
# The purpose of this class is to store Build related runner session. # The purpose of this class is to store Build related runner session.
# Data will be removed after transitioning from running to any state. # Data will be removed after transitioning from running to any state.
......
# frozen_string_literal: true
module Ci module Ci
class BuildTraceChunk < ActiveRecord::Base class BuildTraceChunk < ActiveRecord::Base
include FastDestroyAll include FastDestroyAll
......
# frozen_string_literal: true
module Ci module Ci
module BuildTraceChunks module BuildTraceChunks
class Database class Database
......
# frozen_string_literal: true
module Ci module Ci
module BuildTraceChunks module BuildTraceChunks
class Fog class Fog
......
# frozen_string_literal: true
module Ci module Ci
module BuildTraceChunks module BuildTraceChunks
class Redis class Redis
......
# frozen_string_literal: true
module Ci module Ci
class BuildTraceSection < ActiveRecord::Base class BuildTraceSection < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class BuildTraceSectionName < ActiveRecord::Base class BuildTraceSectionName < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
## ##
# This domain model is a representation of a group of jobs that are related # This domain model is a representation of a group of jobs that are related
......
# frozen_string_literal: true
module Ci module Ci
class GroupVariable < ActiveRecord::Base class GroupVariable < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class JobArtifact < ActiveRecord::Base class JobArtifact < ActiveRecord::Base
prepend EE::Ci::JobArtifact prepend EE::Ci::JobArtifact
......
# frozen_string_literal: true
module Ci module Ci
# Currently this is artificial object, constructed dynamically # Currently this is artificial object, constructed dynamically
# We should migrate this object to actual database record in the future # We should migrate this object to actual database record in the future
......
# frozen_string_literal: true
module Ci module Ci
class Pipeline < ActiveRecord::Base class Pipeline < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class PipelineSchedule < ActiveRecord::Base class PipelineSchedule < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class PipelineScheduleVariable < ActiveRecord::Base class PipelineScheduleVariable < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class PipelineVariable < ActiveRecord::Base class PipelineVariable < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class RunnerNamespace < ActiveRecord::Base class RunnerNamespace < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class RunnerProject < ActiveRecord::Base class RunnerProject < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class Stage < ActiveRecord::Base class Stage < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class Trigger < ActiveRecord::Base class Trigger < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class TriggerRequest < ActiveRecord::Base class TriggerRequest < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci module Ci
class Variable < ActiveRecord::Base class Variable < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Clusters module Clusters
module Applications module Applications
class Helm < ActiveRecord::Base class Helm < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters module Clusters
module Applications module Applications
class Ingress < ActiveRecord::Base class Ingress < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters module Clusters
module Applications module Applications
class Jupyter < ActiveRecord::Base class Jupyter < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters module Clusters
module Applications module Applications
class Prometheus < ActiveRecord::Base class Prometheus < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters module Clusters
module Applications module Applications
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters module Clusters
class Cluster < ActiveRecord::Base class Cluster < ActiveRecord::Base
prepend EE::Clusters::Cluster prepend EE::Clusters::Cluster
......
# frozen_string_literal: true
module Clusters module Clusters
module Concerns module Concerns
module ApplicationCore module ApplicationCore
......
# frozen_string_literal: true
module Clusters module Clusters
module Concerns module Concerns
module ApplicationData module ApplicationData
......
# frozen_string_literal: true
module Clusters module Clusters
module Concerns module Concerns
module ApplicationStatus module ApplicationStatus
......
# frozen_string_literal: true
module Clusters module Clusters
module Platforms module Platforms
class Kubernetes < ActiveRecord::Base class Kubernetes < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters module Clusters
class Project < ActiveRecord::Base class Project < ActiveRecord::Base
self.table_name = 'cluster_projects' self.table_name = 'cluster_projects'
......
# frozen_string_literal: true
module Clusters module Clusters
module Providers module Providers
class Gcp < ActiveRecord::Base class Gcp < ActiveRecord::Base
......
...@@ -13,10 +13,9 @@ module Projects ...@@ -13,10 +13,9 @@ module Projects
detection.updates.each do |update| detection.updates.each do |update|
RepositoryLanguage RepositoryLanguage
.arel_table.update_manager
.where(project_id: project.id) .where(project_id: project.id)
.where(programming_language_id: update[:programming_language_id]) .where(programming_language_id: update[:programming_language_id])
.set(share: update[:share]) .update_all(share: update[:share])
end end
Gitlab::Database.bulk_insert( Gitlab::Database.bulk_insert(
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
%br %br
- if @target_url - if @target_url
- if @reply_by_email - if @reply_by_email
= _('Reply to this email directly or %{view_it_on_gitlab}.') % { view_it_on_gitlab: link_to(_("view it on GitLab"), @target_url) } = _('Reply to this email directly or %{view_it_on_gitlab}.').html_safe % { view_it_on_gitlab: link_to(_("view it on GitLab"), @target_url) }
- else - else
#{link_to _("View it on GitLab"), @target_url}. #{link_to _("View it on GitLab"), @target_url}.
%br %br
......
---
title: refactor pipeline job log animation to reduce CPU usage
merge_request: 20915
author:
type: performance
---
title: Ensure links in notifications footer are not escaped
merge_request: 21000
author:
type: fixed
---
title: Enable frozen string for app/models/**/*.rb
merge_request: 21001
author: gfyoung
type: performance
---
title: Enable renaming files and folders in Web IDE
merge_request: 20835
author:
type: added
---
title: 'Rails5: update Rails5 lock for forgotten gem rouge'
merge_request: 21010
author: Jasper Maes
type: fixed
---
title: Update to Rouge 3.2.0, including Terraform and Crystal lexer and bug fixes
merge_request: 20991
author:
type: changed
...@@ -2,7 +2,7 @@ Rails.application.configure do |config| ...@@ -2,7 +2,7 @@ Rails.application.configure do |config|
Warden::Manager.after_set_user(scope: :user) do |user, auth, opts| Warden::Manager.after_set_user(scope: :user) do |user, auth, opts|
Gitlab::Auth::UniqueIpsLimiter.limit_user!(user) Gitlab::Auth::UniqueIpsLimiter.limit_user!(user)
activity = Gitlab::Auth::Activity.new(user, opts) activity = Gitlab::Auth::Activity.new(opts)
case opts[:event] case opts[:event]
when :authentication when :authentication
...@@ -26,16 +26,32 @@ Rails.application.configure do |config| ...@@ -26,16 +26,32 @@ Rails.application.configure do |config|
end end
Warden::Manager.before_failure(scope: :user) do |env, opts| Warden::Manager.before_failure(scope: :user) do |env, opts|
tracker = Gitlab::Auth::BlockedUserTracker.new(env) Gitlab::Auth::Activity.new(opts).user_authentication_failed!
tracker.log_blocked_user_activity! if tracker.user_blocked?
Gitlab::Auth::Activity.new(tracker.user, opts).user_authentication_failed!
end end
Warden::Manager.before_logout(scope: :user) do |user_warden, auth, opts| Warden::Manager.before_logout(scope: :user) do |user, auth, opts|
user = user_warden || auth.user user ||= auth.user
activity = Gitlab::Auth::Activity.new(opts)
tracker = Gitlab::Auth::BlockedUserTracker.new(user, auth)
ActiveSession.destroy(user, auth.request.session.id) ActiveSession.destroy(user, auth.request.session.id)
Gitlab::Auth::Activity.new(user, opts).user_session_destroyed! activity.user_session_destroyed!
##
# It is possible that `before_logout` event is going to be triggered
# multiple times during the request lifecycle. We want to increment
# metrics and write logs only once in that case.
#
# 'warden.auth.*' is our custom hash key that follows usual convention
# of naming keys in the Rack env hash.
#
next if auth.env['warden.auth.user.blocked']
if user.blocked?
activity.user_blocked!
tracker.log_activity!
end
auth.env['warden.auth.user.blocked'] = true
end end
end end
...@@ -67,11 +67,11 @@ pid "/home/git/gitlab/tmp/pids/unicorn.pid" ...@@ -67,11 +67,11 @@ pid "/home/git/gitlab/tmp/pids/unicorn.pid"
stderr_path "/home/git/gitlab/log/unicorn.stderr.log" stderr_path "/home/git/gitlab/log/unicorn.stderr.log"
stdout_path "/home/git/gitlab/log/unicorn.stdout.log" stdout_path "/home/git/gitlab/log/unicorn.stdout.log"
# combine Ruby 2.0.0dev or REE with "preload_app true" for memory savings # Save memory by sharing the application code among multiple Unicorn workers
# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow # with "preload_app true". See:
# https://www.rubydoc.info/gems/unicorn/5.1.0/Unicorn%2FConfigurator:preload_app
# https://brandur.org/ruby-memory#copy-on-write
preload_app true preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
GC.copy_on_write_friendly = true
# Enable this flag to have unicorn test client connections by writing the # Enable this flag to have unicorn test client connections by writing the
# beginning of the HTTP headers before calling the application. This # beginning of the HTTP headers before calling the application. This
......
worker_processes 2 worker_processes 2
timeout 60 timeout 60
preload_app true
check_client_connection false
before_fork do |server, worker| before_fork do |server, worker|
# the following is highly recommended for Rails + "preload_app true"
# as there's no need for the master process to hold a connection
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
if /darwin/ =~ RUBY_PLATFORM if /darwin/ =~ RUBY_PLATFORM
require 'fiddle' require 'fiddle'
...@@ -13,3 +21,12 @@ before_fork do |server, worker| ...@@ -13,3 +21,12 @@ before_fork do |server, worker|
end end
end end
after_fork do |server, worker|
# Unicorn clears out signals before it forks, so rbtrace won't work
# unless it is enabled after the fork.
require 'rbtrace' if ENV['ENABLE_RBTRACE']
# the following is *required* for Rails + "preload_app true",
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
end
...@@ -142,12 +142,6 @@ These task complies with the `BATCH` environment variable to process uploads in ...@@ -142,12 +142,6 @@ These task complies with the `BATCH` environment variable to process uploads in
gitlab-rake "gitlab:uploads:migrate[FileUploader, MergeRequest]" gitlab-rake "gitlab:uploads:migrate[FileUploader, MergeRequest]"
``` ```
Currently this has to be executed manually and it will allow you to
migrate the existing uploads to the object storage, but all new
uploads will still be stored on the local disk. In the future
you will be given an option to define a default storage for all
new files.
--- ---
**In installations from source:** **In installations from source:**
...@@ -200,12 +194,6 @@ _The uploads are stored by default in ...@@ -200,12 +194,6 @@ _The uploads are stored by default in
``` ```
Currently this has to be executed manually and it will allow you to
migrate the existing uploads to the object storage, but all new
uploads will still be stored on the local disk. In the future
you will be given an option to define a default storage for all
new files.
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab" [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab" [restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
[eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Premium" [eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Premium"
......
...@@ -15,6 +15,7 @@ are very appreciative of the work done by translators and proofreaders! ...@@ -15,6 +15,7 @@ are very appreciative of the work done by translators and proofreaders!
- Chinese Traditional, Hong Kong - Chinese Traditional, Hong Kong
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve) - Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Dutch - Dutch
- Emily Hendle - [GitLab](https://gitlab.com/pundachan), [Crowdin](https://crowdin.com/profile/pandachan)
- Esperanto - Esperanto
- French - French
- Davy Defaud - [GitLab](https://gitlab.com/DevDef), [Crowdin](https://crowdin.com/profile/DevDef) - Davy Defaud - [GitLab](https://gitlab.com/DevDef), [Crowdin](https://crowdin.com/profile/DevDef)
......
...@@ -18,8 +18,7 @@ module Gitlab ...@@ -18,8 +18,7 @@ module Gitlab
user_blocked: 'Counter of sign in attempts when user is blocked' user_blocked: 'Counter of sign in attempts when user is blocked'
}.freeze }.freeze
def initialize(user, opts) def initialize(opts)
@user = user
@opts = opts @opts = opts
end end
...@@ -32,8 +31,6 @@ module Gitlab ...@@ -32,8 +31,6 @@ module Gitlab
when :invalid when :invalid
self.class.user_password_invalid_counter_increment! self.class.user_password_invalid_counter_increment!
end end
self.class.user_blocked_counter_increment! if @user&.blocked?
end end
def user_authenticated! def user_authenticated!
...@@ -51,6 +48,10 @@ module Gitlab ...@@ -51,6 +48,10 @@ module Gitlab
end end
end end
def user_blocked!
self.class.user_blocked_counter_increment!
end
def user_session_destroyed! def user_session_destroyed!
self.class.user_session_destroyed_counter_increment! self.class.user_session_destroyed_counter_increment!
end end
......
...@@ -2,58 +2,21 @@ ...@@ -2,58 +2,21 @@
module Gitlab module Gitlab
module Auth module Auth
class BlockedUserTracker class BlockedUserTracker
include Gitlab::Utils::StrongMemoize def initialize(user, auth)
@user = user
ACTIVE_RECORD_REQUEST_PARAMS = 'action_dispatch.request.request_parameters' @auth = auth
def initialize(env)
@env = env
end
def user_blocked?
user&.blocked?
end end
def user def log_activity!
return unless has_user_blocked_message? return unless @user.blocked?
strong_memoize(:user) do Gitlab::AppLogger.info <<~INFO
# Check for either LDAP or regular GitLab account logins "Failed login for blocked user: user=#{@user.username} ip=#{@auth.request.ip}")
login = @env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'username') || INFO
@env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login')
User.by_login(login) if login.present? SystemHooksService.new.execute_hooks_for(@user, :failed_login)
end
rescue TypeError rescue TypeError
end end
def log_blocked_user_activity!
return unless user_blocked?
Gitlab::AppLogger.info("Failed login for blocked user: user=#{user.username} ip=#{@env['REMOTE_ADDR']}")
SystemHooksService.new.execute_hooks_for(user, :failed_login)
true
rescue TypeError
end
private
##
# Devise calls User#active_for_authentication? on the User model and then
# throws an exception to Warden with User#inactive_message:
# https://github.com/plataformatec/devise/blob/v4.2.1/lib/devise/hooks/activatable.rb#L8
#
# Since Warden doesn't pass the user record to the failure handler, we
# need to do a database lookup with the username. We can limit the
# lookups to happen when the user was blocked by checking the inactive
# message passed along by Warden.
#
def has_user_blocked_message?
strong_memoize(:user_blocked_message) do
message = @env.dig('warden.options', :message)
message == User::BLOCKED_MESSAGE
end
end
end end
end end
end end
...@@ -40,7 +40,7 @@ module Gitlab ...@@ -40,7 +40,7 @@ module Gitlab
# Accepts a path in the form of "#{hex_secret}/#{filename}" # Accepts a path in the form of "#{hex_secret}/#{filename}"
def find_correct_path(upload_path) def find_correct_path(upload_path)
upload = Upload.find_by(uploader: 'FileUploader', path: upload_path) upload = Upload.find_by(uploader: 'FileUploader', path: upload_path)
return unless upload && upload.local? return unless upload && upload.local? && upload.model
upload.absolute_path upload.absolute_path
rescue => e rescue => e
......
...@@ -166,7 +166,7 @@ module Gitlab ...@@ -166,7 +166,7 @@ module Gitlab
except_with_prefix: except_with_prefixes.map { |r| encode_binary(r) } except_with_prefix: except_with_prefixes.map { |r| encode_binary(r) }
) )
response = GitalyClient.call(@repository.storage, :ref_service, :delete_refs, request, timeout: GitalyClient.fast_timeout) response = GitalyClient.call(@repository.storage, :ref_service, :delete_refs, request, timeout: GitalyClient.default_timeout)
raise Gitlab::Git::Repository::GitError, response.git_error if response.git_error.present? raise Gitlab::Git::Repository::GitError, response.git_error if response.git_error.present?
end end
......
...@@ -5400,6 +5400,15 @@ msgstr "" ...@@ -5400,6 +5400,15 @@ msgstr ""
msgid "Remove project" msgid "Remove project"
msgstr "" msgstr ""
msgid "Rename"
msgstr ""
msgid "Rename file"
msgstr ""
msgid "Rename folder"
msgstr ""
msgid "Repair authentication" msgid "Repair authentication"
msgstr "" msgstr ""
......
...@@ -75,7 +75,7 @@ describe('new dropdown component', () => { ...@@ -75,7 +75,7 @@ describe('new dropdown component', () => {
it('calls delete action', () => { it('calls delete action', () => {
spyOn(vm, 'deleteEntry'); spyOn(vm, 'deleteEntry');
vm.$el.querySelectorAll('.dropdown-menu button')[3].click(); vm.$el.querySelectorAll('.dropdown-menu button')[4].click();
expect(vm.deleteEntry).toHaveBeenCalledWith(''); expect(vm.deleteEntry).toHaveBeenCalledWith('');
}); });
......
...@@ -15,7 +15,7 @@ describe('new file modal component', () => { ...@@ -15,7 +15,7 @@ describe('new file modal component', () => {
describe(type, () => { describe(type, () => {
beforeEach(() => { beforeEach(() => {
const store = createStore(); const store = createStore();
store.state.newEntryModal = { store.state.entryModal = {
type, type,
path: '', path: '',
}; };
...@@ -45,7 +45,7 @@ describe('new file modal component', () => { ...@@ -45,7 +45,7 @@ describe('new file modal component', () => {
it('$emits create', () => { it('$emits create', () => {
spyOn(vm, 'createTempEntry'); spyOn(vm, 'createTempEntry');
vm.createEntryInStore(); vm.submitForm();
expect(vm.createTempEntry).toHaveBeenCalledWith({ expect(vm.createTempEntry).toHaveBeenCalledWith({
name: 'testing', name: 'testing',
...@@ -55,4 +55,47 @@ describe('new file modal component', () => { ...@@ -55,4 +55,47 @@ describe('new file modal component', () => {
}); });
}); });
}); });
describe('rename entry', () => {
beforeEach(() => {
const store = createStore();
store.state.entryModal = {
type: 'rename',
path: '',
entry: {
name: 'test',
type: 'blob',
},
};
vm = createComponentWithStore(Component, store).$mount();
});
['tree', 'blob'].forEach(type => {
it(`renders title and button for renaming ${type}`, done => {
const text = type === 'tree' ? 'folder' : 'file';
vm.$store.state.entryModal.entry.type = type;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Rename ${text}`);
expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Rename ${text}`);
done();
});
});
});
describe('entryName', () => {
it('returns entries name', () => {
expect(vm.entryName).toBe('test');
});
it('updated name', () => {
vm.name = 'index.js';
expect(vm.entryName).toBe('index.js');
});
});
});
}); });
...@@ -8,6 +8,7 @@ import actions, { ...@@ -8,6 +8,7 @@ import actions, {
updateTempFlagForEntry, updateTempFlagForEntry,
setErrorMessage, setErrorMessage,
deleteEntry, deleteEntry,
renameEntry,
} from '~/ide/stores/actions'; } from '~/ide/stores/actions';
import store from '~/ide/stores'; import store from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types'; import * as types from '~/ide/stores/mutation_types';
...@@ -468,7 +469,61 @@ describe('Multi-file store actions', () => { ...@@ -468,7 +469,61 @@ describe('Multi-file store actions', () => {
'path', 'path',
store.state, store.state,
[{ type: types.DELETE_ENTRY, payload: 'path' }], [{ type: types.DELETE_ENTRY, payload: 'path' }],
[{ type: 'burstUnusedSeal' }, { type: 'closeFile', payload: store.state.entries.path }], [{ type: 'burstUnusedSeal' }],
done,
);
});
});
describe('renameEntry', () => {
it('renames entry', done => {
store.state.entries.test = {
tree: [],
};
testAction(
renameEntry,
{ path: 'test', name: 'new-name' },
store.state,
[
{
type: types.RENAME_ENTRY,
payload: { path: 'test', name: 'new-name', entryPath: null },
},
],
[{ type: 'deleteEntry', payload: 'test' }],
done,
);
});
it('renames all entries in tree', done => {
store.state.entries.test = {
type: 'tree',
tree: [
{
path: 'tree-1',
},
{
path: 'tree-2',
},
],
};
testAction(
renameEntry,
{ path: 'test', name: 'new-name' },
store.state,
[
{
type: types.RENAME_ENTRY,
payload: { path: 'test', name: 'new-name', entryPath: null },
},
],
[
{ type: 'renameEntry', payload: { path: 'test', name: 'new-name', entryPath: 'tree-1' } },
{ type: 'renameEntry', payload: { path: 'test', name: 'new-name', entryPath: 'tree-2' } },
{ type: 'deleteEntry', payload: 'test' },
],
done, done,
); );
}); });
......
...@@ -294,9 +294,10 @@ describe('IDE commit module actions', () => { ...@@ -294,9 +294,10 @@ describe('IDE commit module actions', () => {
{ {
action: 'update', action: 'update',
file_path: jasmine.anything(), file_path: jasmine.anything(),
content: jasmine.anything(), content: undefined,
encoding: jasmine.anything(), encoding: jasmine.anything(),
last_commit_id: undefined, last_commit_id: undefined,
previous_path: undefined,
}, },
], ],
start_branch: 'master', start_branch: 'master',
...@@ -320,9 +321,10 @@ describe('IDE commit module actions', () => { ...@@ -320,9 +321,10 @@ describe('IDE commit module actions', () => {
{ {
action: 'update', action: 'update',
file_path: jasmine.anything(), file_path: jasmine.anything(),
content: jasmine.anything(), content: undefined,
encoding: jasmine.anything(), encoding: jasmine.anything(),
last_commit_id: '123456789', last_commit_id: '123456789',
previous_path: undefined,
}, },
], ],
start_branch: undefined, start_branch: undefined,
......
...@@ -206,6 +206,7 @@ describe('Multi-file store mutations', () => { ...@@ -206,6 +206,7 @@ describe('Multi-file store mutations', () => {
it('adds to changedFiles', () => { it('adds to changedFiles', () => {
localState.entries.filePath = { localState.entries.filePath = {
deleted: false, deleted: false,
type: 'blob',
}; };
mutations.DELETE_ENTRY(localState, 'filePath'); mutations.DELETE_ENTRY(localState, 'filePath');
...@@ -213,4 +214,103 @@ describe('Multi-file store mutations', () => { ...@@ -213,4 +214,103 @@ describe('Multi-file store mutations', () => {
expect(localState.changedFiles).toEqual([localState.entries.filePath]); expect(localState.changedFiles).toEqual([localState.entries.filePath]);
}); });
}); });
describe('UPDATE_FILE_AFTER_COMMIT', () => {
it('updates URLs if prevPath is set', () => {
const f = {
...file(),
path: 'test',
prevPath: 'testing-123',
rawPath: `${gl.TEST_HOST}/testing-123`,
permalink: `${gl.TEST_HOST}/testing-123`,
commitsPath: `${gl.TEST_HOST}/testing-123`,
blamePath: `${gl.TEST_HOST}/testing-123`,
};
localState.entries.test = f;
localState.changedFiles.push(f);
mutations.UPDATE_FILE_AFTER_COMMIT(localState, { file: f, lastCommit: { commit: {} } });
expect(f.rawPath).toBe(`${gl.TEST_HOST}/test`);
expect(f.permalink).toBe(`${gl.TEST_HOST}/test`);
expect(f.commitsPath).toBe(`${gl.TEST_HOST}/test`);
expect(f.blamePath).toBe(`${gl.TEST_HOST}/test`);
});
});
describe('OPEN_NEW_ENTRY_MODAL', () => {
it('sets entryModal', () => {
localState.entries.testPath = {
...file(),
};
mutations.OPEN_NEW_ENTRY_MODAL(localState, { type: 'test', path: 'testPath' });
expect(localState.entryModal).toEqual({
type: 'test',
path: 'testPath',
entry: localState.entries.testPath,
});
});
});
describe('RENAME_ENTRY', () => {
beforeEach(() => {
localState.trees = {
'gitlab-ce/master': { tree: [] },
};
localState.currentProjectId = 'gitlab-ce';
localState.currentBranchId = 'master';
localState.entries.oldPath = {
...file(),
type: 'blob',
name: 'oldPath',
path: 'oldPath',
url: `${gl.TEST_HOST}/oldPath`,
};
});
it('creates new renamed entry', () => {
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
expect(localState.entries.newPath).toEqual({
...localState.entries.oldPath,
id: 'newPath',
name: 'newPath',
key: 'newPath-blob-name',
path: 'newPath',
tempFile: true,
prevPath: 'oldPath',
tree: [],
parentPath: '',
url: `${gl.TEST_HOST}/newPath`,
moved: jasmine.anything(),
movedPath: jasmine.anything(),
});
});
it('adds new entry to changedFiles', () => {
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
expect(localState.changedFiles.length).toBe(1);
expect(localState.changedFiles[0].path).toBe('newPath');
});
it('sets oldEntry as moved', () => {
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
expect(localState.entries.oldPath.moved).toBe(true);
});
it('adds to parents tree', () => {
localState.entries.oldPath.parentPath = 'parentPath';
localState.entries.parentPath = {
...file(),
};
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
expect(localState.entries.parentPath.tree.length).toBe(1);
});
});
}); });
...@@ -112,6 +112,7 @@ describe('Multi-file store utils', () => { ...@@ -112,6 +112,7 @@ describe('Multi-file store utils', () => {
content: 'updated file content', content: 'updated file content',
encoding: 'text', encoding: 'text',
last_commit_id: '123456789', last_commit_id: '123456789',
previous_path: undefined,
}, },
{ {
action: 'create', action: 'create',
...@@ -119,13 +120,15 @@ describe('Multi-file store utils', () => { ...@@ -119,13 +120,15 @@ describe('Multi-file store utils', () => {
content: 'new file content', content: 'new file content',
encoding: 'base64', encoding: 'base64',
last_commit_id: '123456789', last_commit_id: '123456789',
previous_path: undefined,
}, },
{ {
action: 'delete', action: 'delete',
file_path: 'deletedFile', file_path: 'deletedFile',
content: '', content: undefined,
encoding: 'text', encoding: 'text',
last_commit_id: undefined, last_commit_id: undefined,
previous_path: undefined,
}, },
], ],
start_branch: undefined, start_branch: undefined,
...@@ -172,6 +175,7 @@ describe('Multi-file store utils', () => { ...@@ -172,6 +175,7 @@ describe('Multi-file store utils', () => {
content: 'updated file content', content: 'updated file content',
encoding: 'text', encoding: 'text',
last_commit_id: '123456789', last_commit_id: '123456789',
previous_path: undefined,
}, },
{ {
action: 'create', action: 'create',
...@@ -179,6 +183,7 @@ describe('Multi-file store utils', () => { ...@@ -179,6 +183,7 @@ describe('Multi-file store utils', () => {
content: 'new file content', content: 'new file content',
encoding: 'base64', encoding: 'base64',
last_commit_id: '123456789', last_commit_id: '123456789',
previous_path: undefined,
}, },
], ],
start_branch: undefined, start_branch: undefined,
...@@ -195,13 +200,17 @@ describe('Multi-file store utils', () => { ...@@ -195,13 +200,17 @@ describe('Multi-file store utils', () => {
expect(utils.commitActionForFile({ tempFile: true })).toBe('create'); expect(utils.commitActionForFile({ tempFile: true })).toBe('create');
}); });
it('returns move for moved file', () => {
expect(utils.commitActionForFile({ prevPath: 'test' })).toBe('move');
});
it('returns update by default', () => { it('returns update by default', () => {
expect(utils.commitActionForFile({})).toBe('update'); expect(utils.commitActionForFile({})).toBe('update');
}); });
}); });
describe('getCommitFiles', () => { describe('getCommitFiles', () => {
it('returns flattened list of files and folders', () => { it('returns list of files excluding moved files', () => {
const files = [ const files = [
{ {
path: 'a', path: 'a',
...@@ -209,19 +218,9 @@ describe('Multi-file store utils', () => { ...@@ -209,19 +218,9 @@ describe('Multi-file store utils', () => {
deleted: true, deleted: true,
}, },
{ {
path: 'b', path: 'c',
type: 'tree', type: 'blob',
deleted: true, moved: true,
tree: [
{
path: 'c',
type: 'blob',
},
{
path: 'd',
type: 'blob',
},
],
}, },
]; ];
...@@ -233,16 +232,6 @@ describe('Multi-file store utils', () => { ...@@ -233,16 +232,6 @@ describe('Multi-file store utils', () => {
type: 'blob', type: 'blob',
deleted: true, deleted: true,
}, },
{
path: 'c',
type: 'blob',
deleted: true,
},
{
path: 'd',
type: 'blob',
deleted: true,
},
]); ]);
}); });
}); });
......
...@@ -29,7 +29,7 @@ describe('GlModal', () => { ...@@ -29,7 +29,7 @@ describe('GlModal', () => {
describe('without id', () => { describe('without id', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(modalComponent, { }); vm = mountComponent(modalComponent, {});
}); });
it('does not add an id attribute to the modal', () => { it('does not add an id attribute to the modal', () => {
...@@ -83,7 +83,7 @@ describe('GlModal', () => { ...@@ -83,7 +83,7 @@ describe('GlModal', () => {
}); });
}); });
it('works with data-toggle="modal"', (done) => { it('works with data-toggle="modal"', done => {
setFixtures(` setFixtures(`
<button id="modal-button" data-toggle="modal" data-target="#my-modal"></button> <button id="modal-button" data-toggle="modal" data-target="#my-modal"></button>
<div id="modal-container"></div> <div id="modal-container"></div>
...@@ -91,9 +91,13 @@ describe('GlModal', () => { ...@@ -91,9 +91,13 @@ describe('GlModal', () => {
const modalContainer = document.getElementById('modal-container'); const modalContainer = document.getElementById('modal-container');
const modalButton = document.getElementById('modal-button'); const modalButton = document.getElementById('modal-button');
vm = mountComponent(modalComponent, { vm = mountComponent(
id: 'my-modal', modalComponent,
}, modalContainer); {
id: 'my-modal',
},
modalContainer,
);
$(vm.$el).on('shown.bs.modal', () => done()); $(vm.$el).on('shown.bs.modal', () => done());
modalButton.click(); modalButton.click();
...@@ -103,7 +107,7 @@ describe('GlModal', () => { ...@@ -103,7 +107,7 @@ describe('GlModal', () => {
const dummyEvent = 'not really an event'; const dummyEvent = 'not really an event';
beforeEach(() => { beforeEach(() => {
vm = mountComponent(modalComponent, { }); vm = mountComponent(modalComponent, {});
spyOn(vm, '$emit'); spyOn(vm, '$emit');
}); });
...@@ -122,11 +126,27 @@ describe('GlModal', () => { ...@@ -122,11 +126,27 @@ describe('GlModal', () => {
expect(vm.$emit).toHaveBeenCalledWith('submit', dummyEvent); expect(vm.$emit).toHaveBeenCalledWith('submit', dummyEvent);
}); });
}); });
describe('opened', () => {
it('emits a open event', () => {
vm.opened();
expect(vm.$emit).toHaveBeenCalledWith('open');
});
});
describe('closed', () => {
it('emits a closed event', () => {
vm.closed();
expect(vm.$emit).toHaveBeenCalledWith('closed');
});
});
}); });
describe('slots', () => { describe('slots', () => {
const slotContent = 'this should go into the slot'; const slotContent = 'this should go into the slot';
const modalWithSlot = (slotName) => { const modalWithSlot = slotName => {
let template; let template;
if (slotName) { if (slotName) {
template = ` template = `
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Auth::BlockedUserTracker do describe Gitlab::Auth::BlockedUserTracker do
set(:user) { create(:user) }
describe '#log_blocked_user_activity!' do describe '#log_blocked_user_activity!' do
it 'does not log if user failed to login due to undefined reason' do context 'when user is not blocked' do
expect_any_instance_of(SystemHooksService).not_to receive(:execute_hooks_for) it 'does not log blocked user activity' do
expect_any_instance_of(SystemHooksService)
tracker = described_class.new({}) .not_to receive(:execute_hooks_for)
expect(Gitlab::AppLogger).not_to receive(:info)
expect(tracker.user).to be_nil user = create(:user)
expect(tracker.user_blocked?).to be_falsey
expect(tracker.log_blocked_user_activity!).to be_nil
end
it 'gracefully handles malformed environment variables' do described_class.new(user, spy('auth')).log_activity!
tracker = described_class.new({ 'warden.options' => 'test' })
expect(tracker.user).to be_nil
expect(tracker.user_blocked?).to be_falsey
expect(tracker.log_blocked_user_activity!).to be_nil
end
context 'failed login due to blocked user' do
let(:base_env) { { 'warden.options' => { message: User::BLOCKED_MESSAGE } } }
let(:env) { base_env.merge(request_env) }
subject { described_class.new(env) }
before do
expect_any_instance_of(SystemHooksService).to receive(:execute_hooks_for).with(user, :failed_login)
end end
end
context 'via GitLab login' do context 'when user is not blocked' do
let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => user.username } } } } it 'logs blocked user activity' do
user = create(:user, :blocked)
it 'logs a blocked user' do
user.block!
expect(subject.user).to be_blocked
expect(subject.user_blocked?).to be true
expect(subject.log_blocked_user_activity!).to be_truthy
end
it 'logs a blocked user by e-mail' do
user.block!
env[described_class::ACTIVE_RECORD_REQUEST_PARAMS]['user']['login'] = user.email
expect(subject.user).to be_blocked
expect(subject.log_blocked_user_activity!).to be_truthy
end
end
context 'via LDAP login' do
let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'username' => user.username } } }
it 'logs a blocked user' do
user.block!
expect(subject.user).to be_blocked
expect(subject.user_blocked?).to be true
expect(subject.log_blocked_user_activity!).to be_truthy
end
it 'logs a LDAP blocked user' do expect_any_instance_of(SystemHooksService)
user.ldap_block! .to receive(:execute_hooks_for)
.with(user, :failed_login)
expect(Gitlab::AppLogger).to receive(:info)
.with(/Failed login for blocked user/)
expect(subject.user).to be_blocked described_class.new(user, spy('auth')).log_activity!
expect(subject.user_blocked?).to be true
expect(subject.log_blocked_user_activity!).to be_truthy
end
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Cleanup::ProjectUploads do
subject { described_class.new(logger: logger) }
let(:logger) { double(:logger) }
before do
allow(logger).to receive(:info).at_least(1).times
allow(logger).to receive(:debug).at_least(1).times
end
describe '#run!' do
shared_examples_for 'moves the file' do
shared_examples_for 'a real run' do
let(:args) { [dry_run: false] }
it 'moves the file to its proper location' do
subject.run!(*args)
expect(File.exist?(path)).to be_falsey
expect(File.exist?(new_path)).to be_truthy
end
it 'logs action as done' do
expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up...")
expect(logger).to receive(:info).with("Did #{action}")
subject.run!(*args)
end
end
shared_examples_for 'a dry run' do
it 'does not move the file' do
subject.run!(*args)
expect(File.exist?(path)).to be_truthy
expect(File.exist?(new_path)).to be_falsey
end
it 'logs action as able to be done' do
expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up. Dry run...")
expect(logger).to receive(:info).with("Can #{action}")
subject.run!(*args)
end
end
context 'when dry_run is false' do
let(:args) { [dry_run: false] }
it_behaves_like 'a real run'
end
context 'when dry_run is nil' do
let(:args) { [dry_run: nil] }
it_behaves_like 'a real run'
end
context 'when dry_run is true' do
let(:args) { [dry_run: true] }
it_behaves_like 'a dry run'
end
context 'with dry_run not specified' do
let(:args) { [] }
it_behaves_like 'a dry run'
end
end
shared_examples_for 'moves the file to lost and found' do
let(:action) { "move to lost and found #{path} -> #{new_path}" }
it_behaves_like 'moves the file'
end
shared_examples_for 'fixes the file' do
let(:action) { "fix #{path} -> #{new_path}" }
it_behaves_like 'moves the file'
end
context 'orphaned project upload file' do
context 'when an upload record matching the secret and filename is found' do
context 'when the project is still in legacy storage' do
let(:orphaned) { create(:upload, :issuable_upload, :with_file, model: create(:project, :legacy_storage)) }
let(:new_path) { orphaned.absolute_path }
let(:path) { File.join(FileUploader.root, 'some', 'wrong', 'location', orphaned.path) }
before do
FileUtils.mkdir_p(File.dirname(path))
FileUtils.mv(new_path, path)
end
it_behaves_like 'fixes the file'
end
context 'when the project was moved to hashed storage' do
let(:orphaned) { create(:upload, :issuable_upload, :with_file) }
let(:new_path) { orphaned.absolute_path }
let(:path) { File.join(FileUploader.root, 'some', 'wrong', 'location', orphaned.path) }
before do
FileUtils.mkdir_p(File.dirname(path))
FileUtils.mv(new_path, path)
end
it_behaves_like 'fixes the file'
end
context 'when the project is missing (the upload *record* is an orphan)' do
let(:orphaned) { create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage)) }
let!(:path) { orphaned.absolute_path }
let!(:new_path) { File.join(FileUploader.root, '-', 'project-lost-found', orphaned.model.full_path, orphaned.path) }
before do
orphaned.model.delete
end
it_behaves_like 'moves the file to lost and found'
end
# We will probably want to add logic (Reschedule background upload) to
# cover Case 2 in https://gitlab.com/gitlab-org/gitlab-ce/issues/46535#note_75355104
context 'when the file should be in object storage' do
context 'when the file otherwise has the correct local path' do
let!(:orphaned) { create(:upload, :issuable_upload, :object_storage, model: build(:project, :legacy_storage)) }
let!(:path) { File.join(FileUploader.root, orphaned.model.full_path, orphaned.path) }
before do
stub_feature_flags(import_export_object_storage: true)
stub_uploads_object_storage(FileUploader)
FileUtils.mkdir_p(File.dirname(path))
FileUtils.touch(path)
end
it 'does not move the file' do
expect(File.exist?(path)).to be_truthy
subject.run!(dry_run: false)
expect(File.exist?(path)).to be_truthy
end
end
# E.g. the upload file was orphaned, and then uploads were migrated to
# object storage
context 'when the file has the wrong local path' do
let!(:orphaned) { create(:upload, :issuable_upload, :object_storage, model: build(:project, :legacy_storage)) }
let!(:path) { File.join(FileUploader.root, 'wrong', orphaned.path) }
let!(:new_path) { File.join(FileUploader.root, '-', 'project-lost-found', 'wrong', orphaned.path) }
before do
stub_feature_flags(import_export_object_storage: true)
stub_uploads_object_storage(FileUploader)
FileUtils.mkdir_p(File.dirname(path))
FileUtils.touch(path)
end
it_behaves_like 'moves the file to lost and found'
end
end
end
context 'when a matching upload record can not be found' do
context 'when the file path fits the known pattern' do
let!(:orphaned) { create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage)) }
let!(:path) { orphaned.absolute_path }
let!(:new_path) { File.join(FileUploader.root, '-', 'project-lost-found', orphaned.model.full_path, orphaned.path) }
before do
orphaned.delete
end
it_behaves_like 'moves the file to lost and found'
end
context 'when the file path does not fit the known pattern' do
let!(:invalid_path) { File.join('group', 'file.jpg') }
let!(:path) { File.join(FileUploader.root, invalid_path) }
let!(:new_path) { File.join(FileUploader.root, '-', 'project-lost-found', invalid_path) }
before do
FileUtils.mkdir_p(File.dirname(path))
FileUtils.touch(path)
end
after do
File.delete(path) if File.exist?(path)
end
it_behaves_like 'moves the file to lost and found'
end
end
end
context 'non-orphaned project upload file' do
it 'does not move the file' do
tracked = create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage))
tracked_path = tracked.absolute_path
expect(logger).not_to receive(:info).with(/move|fix/i)
expect(File.exist?(tracked_path)).to be_truthy
subject.run!(dry_run: false)
expect(File.exist?(tracked_path)).to be_truthy
end
end
context 'ignorable cases' do
# Because we aren't concerned about these, and can save a lot of
# processing time by ignoring them. If we wish to cleanup hashed storage
# directories, it should simply require removing this test and modifying
# the find command.
context 'when the file is already in hashed storage' do
let(:project) { create(:project) }
before do
expect(logger).not_to receive(:info).with(/move|fix/i)
end
it 'does not move even an orphan file' do
orphaned = create(:upload, :issuable_upload, :with_file, model: project)
path = orphaned.absolute_path
orphaned.delete
expect(File.exist?(path)).to be_truthy
subject.run!(dry_run: false)
expect(File.exist?(path)).to be_truthy
end
end
it 'does not move any non-project (FileUploader) uploads' do
paths = []
orphaned1 = create(:upload, :personal_snippet_upload, :with_file)
orphaned2 = create(:upload, :namespace_upload, :with_file)
orphaned3 = create(:upload, :attachment_upload, :with_file)
paths << orphaned1.absolute_path
paths << orphaned2.absolute_path
paths << orphaned3.absolute_path
Upload.delete_all
expect(logger).not_to receive(:info).with(/move|fix/i)
paths.each do |path|
expect(File.exist?(path)).to be_truthy
end
subject.run!(dry_run: false)
paths.each do |path|
expect(File.exist?(path)).to be_truthy
end
end
it 'does not move any uploads in tmp (which would interfere with ongoing upload activity)' do
path = File.join(FileUploader.root, 'tmp', 'foo.jpg')
FileUtils.mkdir_p(File.dirname(path))
FileUtils.touch(path)
expect(logger).not_to receive(:info).with(/move|fix/i)
expect(File.exist?(path)).to be_truthy
subject.run!(dry_run: false)
expect(File.exist?(path)).to be_truthy
end
end
end
end
shared_examples 'gitlab projects import validations' do
context 'with an invalid path' do
let(:path) { '/invalid-path/' }
it 'returns an invalid project' do
project = subject.execute
expect(project).not_to be_persisted
expect(project).not_to be_valid
end
end
context 'with a valid path' do
it 'creates a project' do
project = subject.execute
expect(project).to be_persisted
expect(project).to be_valid
end
end
context 'override params' do
it 'stores them as import data when passed' do
project = described_class
.new(namespace.owner, import_params, description: 'Hello')
.execute
expect(project.import_data.data['override_params']['description']).to eq('Hello')
end
end
context 'when there is a project with the same path' do
let(:existing_project) { create(:project, namespace: namespace) }
let(:path) { existing_project.path}
it 'does not create the project' do
project = subject.execute
expect(project).to be_invalid
expect(project).not_to be_persisted
end
context 'when overwrite param is set' do
let(:overwrite) { true }
it 'creates a project in a temporary full_path' do
project = subject.execute
expect(project).to be_valid
expect(project).to be_persisted
end
end
end
end
...@@ -87,6 +87,10 @@ shared_examples 'an email starting a new thread with reply-by-email enabled' do ...@@ -87,6 +87,10 @@ shared_examples 'an email starting a new thread with reply-by-email enabled' do
include_examples 'an email with X-GitLab headers containing project details' include_examples 'an email with X-GitLab headers containing project details'
include_examples 'a new thread email with reply-by-email enabled' include_examples 'a new thread email with reply-by-email enabled'
it 'includes "Reply to this email directly or <View it on GitLab>"' do
expect(subject.default_part.body).to include(%(Reply to this email directly or <a href="#{Gitlab::UrlBuilder.build(model)}">view it on GitLab</a>.))
end
context 'when reply-by-email is enabled with incoming address with %{key}' do context 'when reply-by-email is enabled with incoming address with %{key}' do
it 'has a Reply-To header' do it 'has a Reply-To header' do
is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/ is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
......
...@@ -68,317 +68,86 @@ describe 'gitlab:cleanup rake tasks' do ...@@ -68,317 +68,86 @@ describe 'gitlab:cleanup rake tasks' do
end end
end end
# A single integration test that is redundant with one part of the
# Gitlab::Cleanup::ProjectUploads spec.
#
# Additionally, this tests DRY_RUN env var values, and the extra line of
# output that says you can disable DRY_RUN if it's enabled.
describe 'cleanup:project_uploads' do describe 'cleanup:project_uploads' do
context 'orphaned project upload file' do let!(:logger) { double(:logger) }
context 'when an upload record matching the secret and filename is found' do
context 'when the project is still in legacy storage' do
let!(:orphaned) { create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage)) }
let!(:correct_path) { orphaned.absolute_path }
let!(:other_project) { create(:project, :legacy_storage) }
let!(:orphaned_path) { correct_path.sub(/#{orphaned.model.full_path}/, other_project.full_path) }
before do before do
FileUtils.mkdir_p(File.dirname(orphaned_path)) expect(main_object).to receive(:logger).and_return(logger).at_least(1).times
FileUtils.mv(correct_path, orphaned_path)
end
it 'moves the file to its proper location' do
expect(Rails.logger).to receive(:info).twice
expect(Rails.logger).to receive(:info).with("Did fix #{orphaned_path} -> #{correct_path}")
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(correct_path)).to be_falsey
stub_env('DRY_RUN', 'false')
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(orphaned_path)).to be_falsey
expect(File.exist?(correct_path)).to be_truthy
end
it 'a dry run does not move the file' do
expect(Rails.logger).to receive(:info).twice
expect(Rails.logger).to receive(:info).with("Can fix #{orphaned_path} -> #{correct_path}")
expect(Rails.logger).to receive(:info)
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(correct_path)).to be_falsey
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(correct_path)).to be_falsey
end
context 'when the project record is missing (Upload#absolute_path raises error)' do
let!(:lost_and_found_path) { File.join(FileUploader.root, '-', 'project-lost-found', other_project.full_path, orphaned.path) }
before do
orphaned.model.delete
end
it 'moves the file to lost and found' do
expect(Rails.logger).to receive(:info).twice
expect(Rails.logger).to receive(:info).with("Did move to lost and found #{orphaned_path} -> #{lost_and_found_path}")
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(lost_and_found_path)).to be_falsey
stub_env('DRY_RUN', 'false')
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(orphaned_path)).to be_falsey
expect(File.exist?(lost_and_found_path)).to be_truthy
end
it 'a dry run does not move the file' do
expect(Rails.logger).to receive(:info).twice
expect(Rails.logger).to receive(:info).with("Can move to lost and found #{orphaned_path} -> #{lost_and_found_path}")
expect(Rails.logger).to receive(:info)
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(lost_and_found_path)).to be_falsey
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(lost_and_found_path)).to be_falsey
end
end
end
context 'when the project was moved to hashed storage' do
let!(:orphaned) { create(:upload, :issuable_upload, :with_file) }
let!(:correct_path) { orphaned.absolute_path }
let!(:orphaned_path) { File.join(FileUploader.root, 'foo', 'bar', orphaned.path) }
before do
FileUtils.mkdir_p(File.dirname(orphaned_path))
FileUtils.mv(correct_path, orphaned_path)
end
it 'moves the file to its proper location' do
expect(Rails.logger).to receive(:info).twice
expect(Rails.logger).to receive(:info).with("Did fix #{orphaned_path} -> #{correct_path}")
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(correct_path)).to be_falsey
stub_env('DRY_RUN', 'false')
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(orphaned_path)).to be_falsey
expect(File.exist?(correct_path)).to be_truthy
end
it 'a dry run does not move the file' do
expect(Rails.logger).to receive(:info).twice
expect(Rails.logger).to receive(:info).with("Can fix #{orphaned_path} -> #{correct_path}")
expect(Rails.logger).to receive(:info)
expect(File.exist?(orphaned_path)).to be_truthy allow(logger).to receive(:info).at_least(1).times
expect(File.exist?(correct_path)).to be_falsey allow(logger).to receive(:debug).at_least(1).times
end
run_rake_task('gitlab:cleanup:project_uploads') context 'with a fixable orphaned project upload file' do
let(:orphaned) { create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage)) }
let(:new_path) { orphaned.absolute_path }
let(:path) { File.join(FileUploader.root, 'some', 'wrong', 'location', orphaned.path) }
expect(File.exist?(orphaned_path)).to be_truthy before do
expect(File.exist?(correct_path)).to be_falsey FileUtils.mkdir_p(File.dirname(path))
end FileUtils.mv(new_path, path)
end
end end
context 'when a matching upload record can not be found' do context 'with DRY_RUN disabled' do
context 'when the file path fits the known pattern' do before do
let!(:orphaned) { create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage)) } stub_env('DRY_RUN', 'false')
let!(:orphaned_path) { orphaned.absolute_path }
let!(:lost_and_found_path) { File.join(FileUploader.root, '-', 'project-lost-found', orphaned.model.full_path, orphaned.path) }
before do
orphaned.delete
end
it 'moves the file to lost and found' do
expect(Rails.logger).to receive(:info).twice
expect(Rails.logger).to receive(:info).with("Did move to lost and found #{orphaned_path} -> #{lost_and_found_path}")
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(lost_and_found_path)).to be_falsey
stub_env('DRY_RUN', 'false')
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(orphaned_path)).to be_falsey
expect(File.exist?(lost_and_found_path)).to be_truthy
end
it 'a dry run does not move the file' do
expect(Rails.logger).to receive(:info).twice
expect(Rails.logger).to receive(:info).with("Can move to lost and found #{orphaned_path} -> #{lost_and_found_path}")
expect(Rails.logger).to receive(:info)
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(lost_and_found_path)).to be_falsey
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(lost_and_found_path)).to be_falsey
end
end end
context 'when the file path does not fit the known pattern' do it 'moves the file to its proper location' do
let!(:invalid_path) { File.join('group', 'file.jpg') } run_rake_task('gitlab:cleanup:project_uploads')
let!(:orphaned_path) { File.join(FileUploader.root, invalid_path) }
let!(:lost_and_found_path) { File.join(FileUploader.root, '-', 'project-lost-found', invalid_path) }
before do
FileUtils.mkdir_p(File.dirname(orphaned_path))
FileUtils.touch(orphaned_path)
end
after do
File.delete(orphaned_path) if File.exist?(orphaned_path)
end
it 'moves the file to lost and found' do
expect(Rails.logger).to receive(:info).twice
expect(Rails.logger).to receive(:info).with("Did move to lost and found #{orphaned_path} -> #{lost_and_found_path}")
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(lost_and_found_path)).to be_falsey
stub_env('DRY_RUN', 'false')
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(orphaned_path)).to be_falsey
expect(File.exist?(lost_and_found_path)).to be_truthy
end
it 'a dry run does not move the file' do
expect(Rails.logger).to receive(:info).twice
expect(Rails.logger).to receive(:info).with("Can move to lost and found #{orphaned_path} -> #{lost_and_found_path}")
expect(Rails.logger).to receive(:info)
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(lost_and_found_path)).to be_falsey
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(orphaned_path)).to be_truthy expect(File.exist?(path)).to be_falsey
expect(File.exist?(lost_and_found_path)).to be_falsey expect(File.exist?(new_path)).to be_truthy
end
end end
end
end
context 'non-orphaned project upload file' do
it 'does not move the file' do
tracked = create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage))
tracked_path = tracked.absolute_path
expect(Rails.logger).not_to receive(:info).with(/move|fix/i) it 'logs action as done' do
expect(File.exist?(tracked_path)).to be_truthy expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up...")
expect(logger).to receive(:info).with("Did fix #{path} -> #{new_path}")
stub_env('DRY_RUN', 'false')
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(tracked_path)).to be_truthy
end
end
context 'ignorable cases' do
shared_examples_for 'does not move anything' do
it 'does not move even an orphan file' do
orphaned = create(:upload, :issuable_upload, :with_file, model: project)
orphaned_path = orphaned.absolute_path
orphaned.delete
expect(File.exist?(orphaned_path)).to be_truthy
run_rake_task('gitlab:cleanup:project_uploads') run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(orphaned_path)).to be_truthy
end end
end end
# Because we aren't concerned about these, and can save a lot of shared_examples_for 'does not move the file' do
# processing time by ignoring them. If we wish to cleanup hashed storage it 'does not move the file' do
# directories, it should simply require removing this test and modifying run_rake_task('gitlab:cleanup:project_uploads')
# the find command.
context 'when the file is already in hashed storage' do
let(:project) { create(:project) }
before do expect(File.exist?(path)).to be_truthy
stub_env('DRY_RUN', 'false') expect(File.exist?(new_path)).to be_falsey
expect(Rails.logger).not_to receive(:info).with(/move|fix/i)
end end
it_behaves_like 'does not move anything' it 'logs action as able to be done' do
end expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up. Dry run...")
expect(logger).to receive(:info).with("Can fix #{path} -> #{new_path}")
context 'when DRY_RUN env var is unset' do expect(logger).to receive(:info).with(/To clean up these files run this command with DRY_RUN=false/)
let(:project) { create(:project, :legacy_storage) }
it_behaves_like 'does not move anything' run_rake_task('gitlab:cleanup:project_uploads')
end
end end
context 'when DRY_RUN env var is true' do context 'with DRY_RUN explicitly enabled' do
let(:project) { create(:project, :legacy_storage) }
before do before do
stub_env('DRY_RUN', 'true') stub_env('DRY_RUN', 'true')
end end
it_behaves_like 'does not move anything' it_behaves_like 'does not move the file'
end end
context 'when DRY_RUN env var is foo' do context 'with DRY_RUN set to an unknown value' do
let(:project) { create(:project, :legacy_storage) }
before do before do
stub_env('DRY_RUN', 'foo') stub_env('DRY_RUN', 'foo')
end end
it_behaves_like 'does not move anything' it_behaves_like 'does not move the file'
end end
it 'does not move any non-project (FileUploader) uploads' do context 'with DRY_RUN unset' do
stub_env('DRY_RUN', 'false') it_behaves_like 'does not move the file'
paths = []
orphaned1 = create(:upload, :personal_snippet_upload, :with_file)
orphaned2 = create(:upload, :namespace_upload, :with_file)
orphaned3 = create(:upload, :attachment_upload, :with_file)
paths << orphaned1.absolute_path
paths << orphaned2.absolute_path
paths << orphaned3.absolute_path
Upload.delete_all
expect(Rails.logger).not_to receive(:info).with(/move|fix/i)
paths.each do |path|
expect(File.exist?(path)).to be_truthy
end
run_rake_task('gitlab:cleanup:project_uploads')
paths.each do |path|
expect(File.exist?(path)).to be_truthy
end
end
it 'does not move any uploads in tmp (which would interfere with ongoing upload activity)' do
stub_env('DRY_RUN', 'false')
path = File.join(FileUploader.root, 'tmp', 'foo.jpg')
FileUtils.mkdir_p(File.dirname(path))
FileUtils.touch(path)
expect(Rails.logger).not_to receive(:info).with(/move|fix/i)
expect(File.exist?(path)).to be_truthy
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(path)).to be_truthy
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment