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
retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (3.1.1)
rouge (3.2.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
......
......@@ -783,7 +783,7 @@ GEM
retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (3.1.1)
rouge (3.2.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
......
......@@ -4,6 +4,7 @@ import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue';
import ItemButton from './button.vue';
import { modalTypes } from '../../constants';
export default {
components: {
......@@ -56,6 +57,7 @@ export default {
this.dropdownOpen = !this.dropdownOpen;
},
},
modalTypes,
};
</script>
......@@ -74,7 +76,7 @@ export default {
@click.stop="openDropdown()"
>
<icon
name="hamburger"
name="ellipsis_v"
/>
<icon
name="arrow-down"
......@@ -106,11 +108,20 @@ export default {
class="d-flex"
icon="folder-new"
icon-classes="mr-2"
@click="createNewItem('tree')"
@click="createNewItem($options.modalTypes.tree)"
/>
</li>
<li class="divider"></li>
</template>
<li>
<item-button
:label="__('Rename')"
class="d-flex"
icon="pencil"
icon-classes="mr-2"
@click="createNewItem($options.modalTypes.rename)"
/>
</li>
<li>
<item-button
:label="__('Delete')"
......
......@@ -2,6 +2,7 @@
import { __ } from '~/locale';
import { mapActions, mapState } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { modalTypes } from '../../constants';
export default {
components: {
......@@ -13,42 +14,58 @@ export default {
};
},
computed: {
...mapState(['newEntryModal']),
...mapState(['entryModal']),
entryName: {
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) {
this.name = val;
},
},
modalTitle() {
if (this.newEntryModal.type === 'tree') {
if (this.entryModal.type === modalTypes.tree) {
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');
},
buttonLabel() {
if (this.newEntryModal.type === 'tree') {
if (this.entryModal.type === modalTypes.tree) {
return __('Create directory');
} else if (this.entryModal.type === modalTypes.rename) {
return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
}
return __('Create file');
},
},
methods: {
...mapActions(['createTempEntry']),
createEntryInStore() {
this.createTempEntry({
name: this.name,
type: this.newEntryModal.type,
});
...mapActions(['createTempEntry', 'renameEntry']),
submitForm() {
if (this.entryModal.type === modalTypes.rename) {
this.renameEntry({
path: this.entryModal.entry.path,
name: this.entryName,
});
} else {
this.createTempEntry({
name: this.name,
type: this.entryModal.type,
});
}
},
focusInput() {
setTimeout(() => {
this.$refs.fieldName.focus();
});
this.$refs.fieldName.focus();
},
closedModal() {
this.name = '';
},
},
};
......@@ -60,8 +77,9 @@ export default {
:header-title-text="modalTitle"
:footer-primary-button-text="buttonLabel"
footer-primary-button-variant="success"
@submit="createEntryInStore"
@submit="submitForm"
@open="focusInput"
@closed="closedModal"
>
<div
class="form-group row"
......
......@@ -134,8 +134,7 @@ export default {
.replace(/[/]$/g, '');
// - strip ending "/"
const filePath = this.file.path
.replace(/[/]$/g, '');
const filePath = this.file.path.replace(/[/]$/g, '');
return filePath === routePath;
},
......@@ -194,7 +193,7 @@ export default {
data-container="body"
data-placement="right"
name="file-modified"
css-classes="prepend-left-5 multi-file-modified"
css-classes="prepend-left-5 ide-file-modified"
/>
</span>
<changed-file-icon
......@@ -208,7 +207,6 @@ export default {
</span>
<new-dropdown
:type="file.type"
:branch="file.branchId"
:path="file.path"
:mouse-over="mouseOver"
class="float-right prepend-left-8"
......
......@@ -53,3 +53,8 @@ export const commitItemIconMap = {
class: 'ide-file-deletion',
},
};
export const modalTypes = {
rename: 'rename',
tree: 'tree',
};
......@@ -8,7 +8,7 @@ export default {
});
},
getRawFileData(file) {
if (file.tempFile) {
if (file.tempFile && !file.prevPath) {
return Promise.resolve(file.content);
}
......
......@@ -186,13 +186,39 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
};
export const deleteEntry = ({ commit, dispatch, state }, path) => {
dispatch('burstUnusedSeal');
dispatch('closeFile', state.entries[path]);
const entry = 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);
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 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/file';
export * from './actions/project';
......
......@@ -62,14 +62,14 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
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 });
const url = file.prevPath ? file.url.replace(file.path, file.prevPath) : file.url;
return service
.getFileData(
`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
)
.getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${url.replace('/-/', '/')}`)
.then(({ data, headers }) => {
const normalizedHeaders = normalizeHeaders(headers);
setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
......@@ -101,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
service
.getRawFileData(file)
.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) {
service
.getBaseRawFileData(file, baseSha)
......@@ -176,9 +176,22 @@ export const setFileViewMode = ({ commit }, { file, viewMode }) => {
export const discardFileChanges = ({ dispatch, state, commit, getters }, 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.REMOVE_FILE_FROM_CHANGED, path);
if (file.prevPath) {
dispatch('discardFileChanges', file.prevPath);
}
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, path);
} else if (getters.activeFile && file.path === getters.activeFile.path) {
......
......@@ -89,3 +89,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
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 =>
!!(state.changedFiles.length || state.stagedFiles.length);
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(
f => filePathMatches(f, path) && !getChangedFile(state)(f.path),
f => filePathMatches(f.path, path) && !getChangedFile(state)(f.path),
).length;
return changedFilesCount + stagedFilesCount;
......
......@@ -77,3 +77,6 @@ export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
export const DELETE_ENTRY = 'DELETE_ENTRY';
export const RENAME_ENTRY = 'RENAME_ENTRY';
export const RESTORE_TREE = 'RESTORE_TREE';
......@@ -131,11 +131,14 @@ export default {
},
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
const changedFile = state.changedFiles.find(f => f.path === file.path);
const { prevPath } = file;
Object.assign(state.entries[file.path], {
raw: file.content,
changed: !!changedFile,
staged: false,
prevPath: '',
moved: false,
lastCommit: Object.assign(state.entries[file.path].lastCommit, {
id: lastCommit.commit.id,
url: lastCommit.commit_path,
......@@ -144,6 +147,18 @@ export default {
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) {
Object.assign(state, {
......@@ -169,7 +184,11 @@ export default {
},
[types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) {
Object.assign(state, {
newEntryModal: { type, path },
entryModal: {
type,
path,
entry: { ...state.entries[path] },
},
});
},
[types.DELETE_ENTRY](state, path) {
......@@ -179,8 +198,48 @@ export default {
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
entry.deleted = true;
state.changedFiles = state.changedFiles.concat(entry);
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,
...mergeRequestMutation,
......
......@@ -53,15 +53,25 @@ export default {
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
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], {
raw,
});
if (file.tempFile) {
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;
} else if (openPendingFile.tempFile) {
openPendingFile.content = raw;
}
},
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
......@@ -119,12 +129,14 @@ export default {
[types.DISCARD_FILE_CHANGES](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path);
const entry = state.entries[path];
const { deleted } = entry;
const { deleted, prevPath } = entry;
Object.assign(state.entries[path], {
content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false,
deleted: false,
moved: false,
movedPath: '',
});
if (deleted) {
......@@ -133,6 +145,12 @@ export default {
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
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) {
......
import * as types from '../mutation_types';
import { sortTree } from '../utils';
export default {
[types.TOGGLE_TREE_OPEN](state, path) {
......@@ -36,4 +37,14 @@ export default {
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 () => ({
rightPane: null,
links: {},
errorMessage: null,
newEntryModal: {
entryModal: {
type: '',
path: '',
entry: {},
},
});
......@@ -47,6 +47,9 @@ export const dataStructure = () => ({
lastOpenedAt: 0,
mrChange: null,
deleted: false,
prevPath: '',
movedPath: '',
moved: false,
});
export const decorateData = entity => {
......@@ -107,7 +110,9 @@ export const setPageTitle = title => {
};
export const commitActionForFile = file => {
if (file.deleted) {
if (file.prevPath) {
return 'move';
} else if (file.deleted) {
return 'delete';
} else if (file.tempFile) {
return 'create';
......@@ -116,15 +121,12 @@ export const commitActionForFile = file => {
return 'update';
};
export const getCommitFiles = (stagedFiles, deleteTree = false) =>
export const getCommitFiles = stagedFiles =>
stagedFiles.reduce((acc, file) => {
if ((file.deleted || deleteTree) && file.type === 'tree') {
return acc.concat(getCommitFiles(file.tree, true));
}
if (file.moved) return acc;
return acc.concat({
...file,
deleted: deleteTree || file.deleted,
});
}, []);
......@@ -134,9 +136,10 @@ export const createCommitPayload = ({ branch, getters, newBranch, state, rootSta
actions: getCommitFiles(rootState.stagedFiles).map(f => ({
action: commitActionForFile(f),
file_path: f.path,
content: f.content,
previous_path: f.prevPath === '' ? undefined : f.prevPath,
content: f.content || undefined,
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,
});
......@@ -164,8 +167,7 @@ export const sortTree = sortedTree =>
)
.sort(sortTreesByTypeAndName);
export const filePathMatches = (f, path) =>
f.path.replace(new RegExp(`${f.name}$`), '').indexOf(`${path}/`) === 0;
export const filePathMatches = (filePath, path) => filePath.indexOf(`${path}/`) === 0;
export const getChangesCountForFiles = (files, path) =>
files.filter(f => filePathMatches(f, path)).length;
files.filter(f => filePathMatches(f.path, path)).length;
<script>
import $ from 'jquery';
const buttonVariants = ['danger', 'primary', 'success', 'warning'];
const sizeVariants = ['sm', 'md', 'lg', 'xl'];
......@@ -38,6 +40,12 @@ export default {
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: {
emitCancel(event) {
this.$emit('cancel', event);
......@@ -45,10 +53,11 @@ export default {
emitSubmit(event) {
this.$emit('submit', event);
},
opened({ propertyName }) {
if (propertyName === 'opacity') {
this.$emit('open');
}
opened() {
this.$emit('open');
},
closed() {
this.$emit('closed');
},
},
};
......@@ -60,7 +69,6 @@ export default {
class="modal fade"
tabindex="-1"
role="dialog"
@transitionend="opened"
>
<div
:class="modalSizeClass"
......
......@@ -72,6 +72,15 @@ export default {
is-new
/>
<issues-block
v-if="newIssues.length"
:component="component"
:issues="newIssues"
class="js-mr-code-new-issues"
status="failed"
is-new
/>
<issues-block
v-if="unresolvedIssues.length"
:component="component"
......
......@@ -1377,6 +1377,7 @@
.ide-entry-dropdown-toggle {
padding: $gl-padding-4;
color: $gl-text-color;
background-color: $theme-gray-100;
&:hover {
......@@ -1389,6 +1390,10 @@
background-color: $blue-500;
outline: 0;
}
svg {
fill: currentColor;
}
}
.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle {
......
......@@ -109,24 +109,6 @@
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) {
td {
background-color: $row-hover;
......
......@@ -108,6 +108,7 @@ class ApplicationController < ActionController::Base
def append_info_to_payload(payload)
super
payload[:remote_ip] = request.remote_ip
logged_user = auth_user
......@@ -122,12 +123,16 @@ class ApplicationController < ActionController::Base
end
end
##
# 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
return current_user if current_user.present?
return try(:authenticated_user)
if user_signed_in?
current_user
else
try(:authenticated_user)
end
end
# This filter handles personal access tokens, and atom requests with rss tokens
......
# frozen_string_literal: true
class ApplicationSetting
class Term < ActiveRecord::Base
include CacheMarkdownField
......
# frozen_string_literal: true
class GroupBadge < Badge
belongs_to :group
......
# frozen_string_literal: true
class ProjectBadge < Badge
belongs_to :project
......
# frozen_string_literal: true
module BlobViewer
module Auxiliary
extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer
class Balsamiq < Base
include Rich
......
# frozen_string_literal: true
module BlobViewer
class Base
PARTIAL_PATH_PREFIX = 'projects/blob/viewers'.freeze
......
# frozen_string_literal: true
module BlobViewer
class BinarySTL < Base
include Rich
......
# frozen_string_literal: true
module BlobViewer
class Cartfile < DependencyManager
include Static
......
# frozen_string_literal: true
module BlobViewer
class Changelog < Base
include Auxiliary
......
# frozen_string_literal: true
module BlobViewer
module ClientSide
extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer
class ComposerJson < DependencyManager
include ServerSide
......
# frozen_string_literal: true
module BlobViewer
class Contributing < Base
include Auxiliary
......
# frozen_string_literal: true
module BlobViewer
class DependencyManager < Base
include Auxiliary
......
# frozen_string_literal: true
module BlobViewer
class Download < Base
include Simple
......
# frozen_string_literal: true
module BlobViewer
class Empty < Base
include Simple
......
# frozen_string_literal: true
module BlobViewer
class Gemfile < DependencyManager
include Static
......
# frozen_string_literal: true
module BlobViewer
class Gemspec < DependencyManager
include ServerSide
......
# frozen_string_literal: true
module BlobViewer
class GitlabCiYml < Base
include ServerSide
......
# frozen_string_literal: true
module BlobViewer
class GodepsJson < DependencyManager
include Static
......
# frozen_string_literal: true
module BlobViewer
class Image < Base
include Rich
......
# frozen_string_literal: true
module BlobViewer
class License < Base
include Auxiliary
......
# frozen_string_literal: true
module BlobViewer
class Markup < Base
include Rich
......
# frozen_string_literal: true
module BlobViewer
class Notebook < Base
include Rich
......
# frozen_string_literal: true
module BlobViewer
class PackageJson < DependencyManager
include ServerSide
......
# frozen_string_literal: true
module BlobViewer
class PDF < Base
include Rich
......
# frozen_string_literal: true
module BlobViewer
class Podfile < DependencyManager
include Static
......
# frozen_string_literal: true
module BlobViewer
class Podspec < DependencyManager
include ServerSide
......
# frozen_string_literal: true
module BlobViewer
class PodspecJson < Podspec
self.file_types = %i(podspec_json)
......
# frozen_string_literal: true
module BlobViewer
class Readme < Base
include Auxiliary
......
# frozen_string_literal: true
module BlobViewer
class RequirementsTxt < DependencyManager
include Static
......
# frozen_string_literal: true
module BlobViewer
module Rich
extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer
class RouteMap < Base
include ServerSide
......
# frozen_string_literal: true
module BlobViewer
module ServerSide
extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer
module Simple
extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer
class Sketch < Base
include Rich
......
# frozen_string_literal: true
module BlobViewer
module Static
extend ActiveSupport::Concern
......
# frozen_string_literal: true
module BlobViewer
class SVG < Base
include Rich
......
# frozen_string_literal: true
module BlobViewer
class Text < Base
include Simple
......
# frozen_string_literal: true
module BlobViewer
class TextSTL < BinarySTL
self.binary = false
......
# frozen_string_literal: true
module BlobViewer
class Video < Base
include Rich
......
# frozen_string_literal: true
module BlobViewer
class YarnLock < DependencyManager
include Static
......
# frozen_string_literal: true
module Ci
class ArtifactBlob
include BlobLike
......
# frozen_string_literal: true
module Ci
class Build < CommitStatus
prepend ArtifactMigratable
......
# frozen_string_literal: true
module Ci
# 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.
......
# frozen_string_literal: true
module Ci
# The purpose of this class is to store Build related runner session.
# Data will be removed after transitioning from running to any state.
......
# frozen_string_literal: true
module Ci
class BuildTraceChunk < ActiveRecord::Base
include FastDestroyAll
......
# frozen_string_literal: true
module Ci
module BuildTraceChunks
class Database
......
# frozen_string_literal: true
module Ci
module BuildTraceChunks
class Fog
......
# frozen_string_literal: true
module Ci
module BuildTraceChunks
class Redis
......
# frozen_string_literal: true
module Ci
class BuildTraceSection < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class BuildTraceSectionName < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
##
# This domain model is a representation of a group of jobs that are related
......
# frozen_string_literal: true
module Ci
class GroupVariable < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class JobArtifact < ActiveRecord::Base
prepend EE::Ci::JobArtifact
......
# frozen_string_literal: true
module Ci
# Currently this is artificial object, constructed dynamically
# We should migrate this object to actual database record in the future
......
# frozen_string_literal: true
module Ci
class Pipeline < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class PipelineSchedule < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class PipelineScheduleVariable < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class PipelineVariable < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class Runner < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class RunnerNamespace < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class RunnerProject < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class Stage < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class Trigger < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class TriggerRequest < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Ci
class Variable < ActiveRecord::Base
extend Gitlab::Ci::Model
......
# frozen_string_literal: true
module Clusters
module Applications
class Helm < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters
module Applications
class Ingress < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters
module Applications
class Jupyter < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters
module Applications
class Prometheus < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters
module Applications
class Runner < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters
class Cluster < ActiveRecord::Base
prepend EE::Clusters::Cluster
......
# frozen_string_literal: true
module Clusters
module Concerns
module ApplicationCore
......
# frozen_string_literal: true
module Clusters
module Concerns
module ApplicationData
......
# frozen_string_literal: true
module Clusters
module Concerns
module ApplicationStatus
......
# frozen_string_literal: true
module Clusters
module Platforms
class Kubernetes < ActiveRecord::Base
......
# frozen_string_literal: true
module Clusters
class Project < ActiveRecord::Base
self.table_name = 'cluster_projects'
......
# frozen_string_literal: true
module Clusters
module Providers
class Gcp < ActiveRecord::Base
......
......@@ -13,10 +13,9 @@ module Projects
detection.updates.each do |update|
RepositoryLanguage
.arel_table.update_manager
.where(project_id: project.id)
.where(programming_language_id: update[:programming_language_id])
.set(share: update[:share])
.update_all(share: update[:share])
end
Gitlab::Database.bulk_insert(
......
......@@ -14,7 +14,7 @@
%br
- if @target_url
- 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
#{link_to _("View it on GitLab"), @target_url}.
%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|
Warden::Manager.after_set_user(scope: :user) do |user, auth, opts|
Gitlab::Auth::UniqueIpsLimiter.limit_user!(user)
activity = Gitlab::Auth::Activity.new(user, opts)
activity = Gitlab::Auth::Activity.new(opts)
case opts[:event]
when :authentication
......@@ -26,16 +26,32 @@ Rails.application.configure do |config|
end
Warden::Manager.before_failure(scope: :user) do |env, opts|
tracker = Gitlab::Auth::BlockedUserTracker.new(env)
tracker.log_blocked_user_activity! if tracker.user_blocked?
Gitlab::Auth::Activity.new(tracker.user, opts).user_authentication_failed!
Gitlab::Auth::Activity.new(opts).user_authentication_failed!
end
Warden::Manager.before_logout(scope: :user) do |user_warden, auth, opts|
user = user_warden || auth.user
Warden::Manager.before_logout(scope: :user) do |user, auth, opts|
user ||= auth.user
activity = Gitlab::Auth::Activity.new(opts)
tracker = Gitlab::Auth::BlockedUserTracker.new(user, auth)
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
......@@ -67,11 +67,11 @@ pid "/home/git/gitlab/tmp/pids/unicorn.pid"
stderr_path "/home/git/gitlab/log/unicorn.stderr.log"
stdout_path "/home/git/gitlab/log/unicorn.stdout.log"
# combine Ruby 2.0.0dev or REE with "preload_app true" for memory savings
# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
# Save memory by sharing the application code among multiple Unicorn workers
# 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
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
# beginning of the HTTP headers before calling the application. This
......
worker_processes 2
timeout 60
preload_app true
check_client_connection false
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
require 'fiddle'
......@@ -13,3 +21,12 @@ before_fork do |server, worker|
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
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:**
......@@ -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"
[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
[eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Premium"
......
......@@ -15,6 +15,7 @@ are very appreciative of the work done by translators and proofreaders!
- Chinese Traditional, Hong Kong
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Dutch
- Emily Hendle - [GitLab](https://gitlab.com/pundachan), [Crowdin](https://crowdin.com/profile/pandachan)
- Esperanto
- French
- Davy Defaud - [GitLab](https://gitlab.com/DevDef), [Crowdin](https://crowdin.com/profile/DevDef)
......
......@@ -18,8 +18,7 @@ module Gitlab
user_blocked: 'Counter of sign in attempts when user is blocked'
}.freeze
def initialize(user, opts)
@user = user
def initialize(opts)
@opts = opts
end
......@@ -32,8 +31,6 @@ module Gitlab
when :invalid
self.class.user_password_invalid_counter_increment!
end
self.class.user_blocked_counter_increment! if @user&.blocked?
end
def user_authenticated!
......@@ -51,6 +48,10 @@ module Gitlab
end
end
def user_blocked!
self.class.user_blocked_counter_increment!
end
def user_session_destroyed!
self.class.user_session_destroyed_counter_increment!
end
......
......@@ -2,58 +2,21 @@
module Gitlab
module Auth
class BlockedUserTracker
include Gitlab::Utils::StrongMemoize
ACTIVE_RECORD_REQUEST_PARAMS = 'action_dispatch.request.request_parameters'
def initialize(env)
@env = env
end
def user_blocked?
user&.blocked?
def initialize(user, auth)
@user = user
@auth = auth
end
def user
return unless has_user_blocked_message?
def log_activity!
return unless @user.blocked?
strong_memoize(:user) do
# Check for either LDAP or regular GitLab account logins
login = @env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'username') ||
@env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login')
Gitlab::AppLogger.info <<~INFO
"Failed login for blocked user: user=#{@user.username} ip=#{@auth.request.ip}")
INFO
User.by_login(login) if login.present?
end
SystemHooksService.new.execute_hooks_for(@user, :failed_login)
rescue TypeError
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
......@@ -40,7 +40,7 @@ module Gitlab
# Accepts a path in the form of "#{hex_secret}/#{filename}"
def find_correct_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
rescue => e
......
......@@ -166,7 +166,7 @@ module Gitlab
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?
end
......
......@@ -5400,6 +5400,15 @@ msgstr ""
msgid "Remove project"
msgstr ""
msgid "Rename"
msgstr ""
msgid "Rename file"
msgstr ""
msgid "Rename folder"
msgstr ""
msgid "Repair authentication"
msgstr ""
......
......@@ -75,7 +75,7 @@ describe('new dropdown component', () => {
it('calls delete action', () => {
spyOn(vm, 'deleteEntry');
vm.$el.querySelectorAll('.dropdown-menu button')[3].click();
vm.$el.querySelectorAll('.dropdown-menu button')[4].click();
expect(vm.deleteEntry).toHaveBeenCalledWith('');
});
......
......@@ -15,7 +15,7 @@ describe('new file modal component', () => {
describe(type, () => {
beforeEach(() => {
const store = createStore();
store.state.newEntryModal = {
store.state.entryModal = {
type,
path: '',
};
......@@ -45,7 +45,7 @@ describe('new file modal component', () => {
it('$emits create', () => {
spyOn(vm, 'createTempEntry');
vm.createEntryInStore();
vm.submitForm();
expect(vm.createTempEntry).toHaveBeenCalledWith({
name: 'testing',
......@@ -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, {
updateTempFlagForEntry,
setErrorMessage,
deleteEntry,
renameEntry,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types';
......@@ -468,7 +469,61 @@ describe('Multi-file store actions', () => {
'path',
store.state,
[{ 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,
);
});
......
......@@ -294,9 +294,10 @@ describe('IDE commit module actions', () => {
{
action: 'update',
file_path: jasmine.anything(),
content: jasmine.anything(),
content: undefined,
encoding: jasmine.anything(),
last_commit_id: undefined,
previous_path: undefined,
},
],
start_branch: 'master',
......@@ -320,9 +321,10 @@ describe('IDE commit module actions', () => {
{
action: 'update',
file_path: jasmine.anything(),
content: jasmine.anything(),
content: undefined,
encoding: jasmine.anything(),
last_commit_id: '123456789',
previous_path: undefined,
},
],
start_branch: undefined,
......
......@@ -206,6 +206,7 @@ describe('Multi-file store mutations', () => {
it('adds to changedFiles', () => {
localState.entries.filePath = {
deleted: false,
type: 'blob',
};
mutations.DELETE_ENTRY(localState, 'filePath');
......@@ -213,4 +214,103 @@ describe('Multi-file store mutations', () => {
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', () => {
content: 'updated file content',
encoding: 'text',
last_commit_id: '123456789',
previous_path: undefined,
},
{
action: 'create',
......@@ -119,13 +120,15 @@ describe('Multi-file store utils', () => {
content: 'new file content',
encoding: 'base64',
last_commit_id: '123456789',
previous_path: undefined,
},
{
action: 'delete',
file_path: 'deletedFile',
content: '',
content: undefined,
encoding: 'text',
last_commit_id: undefined,
previous_path: undefined,
},
],
start_branch: undefined,
......@@ -172,6 +175,7 @@ describe('Multi-file store utils', () => {
content: 'updated file content',
encoding: 'text',
last_commit_id: '123456789',
previous_path: undefined,
},
{
action: 'create',
......@@ -179,6 +183,7 @@ describe('Multi-file store utils', () => {
content: 'new file content',
encoding: 'base64',
last_commit_id: '123456789',
previous_path: undefined,
},
],
start_branch: undefined,
......@@ -195,13 +200,17 @@ describe('Multi-file store utils', () => {
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', () => {
expect(utils.commitActionForFile({})).toBe('update');
});
});
describe('getCommitFiles', () => {
it('returns flattened list of files and folders', () => {
it('returns list of files excluding moved files', () => {
const files = [
{
path: 'a',
......@@ -209,19 +218,9 @@ describe('Multi-file store utils', () => {
deleted: true,
},
{
path: 'b',
type: 'tree',
deleted: true,
tree: [
{
path: 'c',
type: 'blob',
},
{
path: 'd',
type: 'blob',
},
],
path: 'c',
type: 'blob',
moved: true,
},
];
......@@ -233,16 +232,6 @@ describe('Multi-file store utils', () => {
type: 'blob',
deleted: true,
},
{
path: 'c',
type: 'blob',
deleted: true,
},
{
path: 'd',
type: 'blob',
deleted: true,
},
]);
});
});
......
......@@ -29,7 +29,7 @@ describe('GlModal', () => {
describe('without id', () => {
beforeEach(() => {
vm = mountComponent(modalComponent, { });
vm = mountComponent(modalComponent, {});
});
it('does not add an id attribute to the modal', () => {
......@@ -83,7 +83,7 @@ describe('GlModal', () => {
});
});
it('works with data-toggle="modal"', (done) => {
it('works with data-toggle="modal"', done => {
setFixtures(`
<button id="modal-button" data-toggle="modal" data-target="#my-modal"></button>
<div id="modal-container"></div>
......@@ -91,9 +91,13 @@ describe('GlModal', () => {
const modalContainer = document.getElementById('modal-container');
const modalButton = document.getElementById('modal-button');
vm = mountComponent(modalComponent, {
id: 'my-modal',
}, modalContainer);
vm = mountComponent(
modalComponent,
{
id: 'my-modal',
},
modalContainer,
);
$(vm.$el).on('shown.bs.modal', () => done());
modalButton.click();
......@@ -103,7 +107,7 @@ describe('GlModal', () => {
const dummyEvent = 'not really an event';
beforeEach(() => {
vm = mountComponent(modalComponent, { });
vm = mountComponent(modalComponent, {});
spyOn(vm, '$emit');
});
......@@ -122,11 +126,27 @@ describe('GlModal', () => {
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', () => {
const slotContent = 'this should go into the slot';
const modalWithSlot = (slotName) => {
const modalWithSlot = slotName => {
let template;
if (slotName) {
template = `
......
require 'spec_helper'
describe Gitlab::Auth::BlockedUserTracker do
set(:user) { create(:user) }
describe '#log_blocked_user_activity!' do
it 'does not log if user failed to login due to undefined reason' do
expect_any_instance_of(SystemHooksService).not_to receive(:execute_hooks_for)
tracker = described_class.new({})
context 'when user is not blocked' do
it 'does not log blocked user activity' do
expect_any_instance_of(SystemHooksService)
.not_to receive(:execute_hooks_for)
expect(Gitlab::AppLogger).not_to receive(:info)
expect(tracker.user).to be_nil
expect(tracker.user_blocked?).to be_falsey
expect(tracker.log_blocked_user_activity!).to be_nil
end
user = create(:user)
it 'gracefully handles malformed environment variables' do
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)
described_class.new(user, spy('auth')).log_activity!
end
end
context 'via GitLab login' do
let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => 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 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
context 'when user is not blocked' do
it 'logs blocked user activity' do
user = create(:user, :blocked)
it 'logs a LDAP blocked user' do
user.ldap_block!
expect_any_instance_of(SystemHooksService)
.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
expect(subject.user_blocked?).to be true
expect(subject.log_blocked_user_activity!).to be_truthy
end
described_class.new(user, spy('auth')).log_activity!
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
include_examples 'an email with X-GitLab headers containing project details'
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
it 'has a Reply-To header' do
is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
......
......@@ -68,317 +68,86 @@ describe 'gitlab:cleanup rake tasks' do
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
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: 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) }
let!(:logger) { double(:logger) }
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
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)
before do
expect(main_object).to receive(:logger).and_return(logger).at_least(1).times
expect(File.exist?(orphaned_path)).to be_truthy
expect(File.exist?(correct_path)).to be_falsey
allow(logger).to receive(:info).at_least(1).times
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
expect(File.exist?(correct_path)).to be_falsey
end
end
before do
FileUtils.mkdir_p(File.dirname(path))
FileUtils.mv(new_path, path)
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!(: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
context 'with DRY_RUN disabled' do
before do
stub_env('DRY_RUN', 'false')
end
context 'when the file path does not fit the known pattern' do
let!(:invalid_path) { File.join('group', 'file.jpg') }
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')
it 'moves the file to its proper location' do
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
expect(File.exist?(path)).to be_falsey
expect(File.exist?(new_path)).to be_truthy
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)
expect(File.exist?(tracked_path)).to be_truthy
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
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 fix #{path} -> #{new_path}")
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(orphaned_path)).to be_truthy
end
end
# 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) }
shared_examples_for 'does not move the file' do
it 'does not move the file' do
run_rake_task('gitlab:cleanup:project_uploads')
before do
stub_env('DRY_RUN', 'false')
expect(Rails.logger).not_to receive(:info).with(/move|fix/i)
expect(File.exist?(path)).to be_truthy
expect(File.exist?(new_path)).to be_falsey
end
it_behaves_like 'does not move anything'
end
context 'when DRY_RUN env var is unset' do
let(:project) { create(:project, :legacy_storage) }
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 fix #{path} -> #{new_path}")
expect(logger).to receive(:info).with(/To clean up these files run this command with DRY_RUN=false/)
it_behaves_like 'does not move anything'
run_rake_task('gitlab:cleanup:project_uploads')
end
end
context 'when DRY_RUN env var is true' do
let(:project) { create(:project, :legacy_storage) }
context 'with DRY_RUN explicitly enabled' do
before do
stub_env('DRY_RUN', 'true')
end
it_behaves_like 'does not move anything'
it_behaves_like 'does not move the file'
end
context 'when DRY_RUN env var is foo' do
let(:project) { create(:project, :legacy_storage) }
context 'with DRY_RUN set to an unknown value' do
before do
stub_env('DRY_RUN', 'foo')
end
it_behaves_like 'does not move anything'
it_behaves_like 'does not move the file'
end
it 'does not move any non-project (FileUploader) uploads' do
stub_env('DRY_RUN', 'false')
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
context 'with DRY_RUN unset' do
it_behaves_like 'does not move the file'
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