summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2018-03-27 11:24:06 +0100
committerPhil Hughes <me@iamphill.com>2018-03-27 11:24:06 +0100
commit74b77ebf50b458ebf9a5e9b5eebb2196c24f1387 (patch)
treec2cf08762b88f4802b548b38cef9e1a7e08ab0fc /app
parent3180f9a6b694066b86c89b3487c13caa1be52b0f (diff)
parent1d7ca033907d8dff18e74c41c391f58382f78ead (diff)
downloadgitlab-ce-74b77ebf50b458ebf9a5e9b5eebb2196c24f1387.tar.gz
Merge branch 'master' into ide-staged-changes
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js4
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js4
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/groups/components/app.vue88
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue118
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue18
-rw-r--r--app/assets/javascripts/ide/lib/editor.js4
-rw-r--r--app/assets/javascripts/notes.js6
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue8
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue8
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue8
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue1
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js21
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue8
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/deprecated_modal.vue (renamed from app/assets/javascripts/vue_shared/components/modal.vue)2
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue8
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss27
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss1
-rw-r--r--app/assets/stylesheets/pages/boards.scss6
-rw-r--r--app/assets/stylesheets/pages/branches.scss21
-rw-r--r--app/assets/stylesheets/pages/events.scss9
-rw-r--r--app/assets/stylesheets/pages/labels.scss8
-rw-r--r--app/assets/stylesheets/pages/projects.scss50
-rw-r--r--app/assets/stylesheets/pages/repo.scss4
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/performance_bar.scss14
-rw-r--r--app/controllers/admin/application_controller.rb14
-rw-r--r--app/controllers/concerns/send_file_upload.rb17
-rw-r--r--app/controllers/concerns/uploads_actions.rb30
-rw-r--r--app/controllers/groups/variables_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb26
-rw-r--r--app/controllers/projects/artifacts_controller.rb12
-rw-r--r--app/controllers/projects/jobs_controller.rb32
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb8
-rw-r--r--app/controllers/projects/pages_controller.rb22
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb17
-rw-r--r--app/controllers/projects/raw_controller.rb3
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/helpers/application_helper.rb7
-rw-r--r--app/helpers/application_settings_helper.rb5
-rw-r--r--app/helpers/projects_helper.rb18
-rw-r--r--app/mailers/emails/merge_requests.rb8
-rw-r--r--app/models/appearance.rb2
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/ci/build.rb18
-rw-r--r--app/models/ci/group_variable.rb2
-rw-r--r--app/models/ci/job_artifact.rb10
-rw-r--r--app/models/ci/pipeline.rb28
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb2
-rw-r--r--app/models/ci/variable.rb2
-rw-r--r--app/models/clusters/cluster.rb2
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/concerns/avatarable.rb3
-rw-r--r--app/models/concerns/deployment_platform.rb22
-rw-r--r--app/models/event.rb10
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/lfs_object.rb9
-rw-r--r--app/models/notification_recipient.rb15
-rw-r--r--app/models/notification_setting.rb7
-rw-r--r--app/models/pages_domain.rb10
-rw-r--r--app/models/project.rb26
-rw-r--r--app/models/project_services/assembla_service.rb4
-rw-r--r--app/models/project_services/bamboo_service.rb12
-rw-r--r--app/models/project_services/buildkite_service.rb2
-rw-r--r--app/models/project_services/campfire_service.rb7
-rw-r--r--app/models/project_services/drone_ci_service.rb2
-rw-r--r--app/models/project_services/external_wiki_service.rb4
-rw-r--r--app/models/project_services/issue_tracker_service.rb4
-rw-r--r--app/models/project_services/mock_ci_service.rb2
-rw-r--r--app/models/project_services/packagist_service.rb2
-rw-r--r--app/models/project_services/pivotaltracker_service.rb4
-rw-r--r--app/models/project_services/pushover_service.rb5
-rw-r--r--app/models/project_services/teamcity_service.rb12
-rw-r--r--app/models/upload.rb19
-rw-r--r--app/models/user.rb13
-rw-r--r--app/services/ci/create_pipeline_service.rb3
-rw-r--r--app/services/ci/create_pipeline_stages_service.rb20
-rw-r--r--app/services/ci/pipeline_trigger_service.rb12
-rw-r--r--app/services/merge_requests/refresh_service.rb8
-rw-r--r--app/services/notification_service.rb10
-rw-r--r--app/services/projects/import_service.rb2
-rw-r--r--app/services/projects/update_pages_configuration_service.rb6
-rw-r--r--app/services/projects/update_pages_service.rb18
-rw-r--r--app/services/projects/update_service.rb10
-rw-r--r--app/services/submit_usage_ping_service.rb5
-rw-r--r--app/services/web_hook_service.rb16
-rw-r--r--app/uploaders/attachment_uploader.rb6
-rw-r--r--app/uploaders/avatar_uploader.rb8
-rw-r--r--app/uploaders/file_mover.rb9
-rw-r--r--app/uploaders/file_uploader.rb56
-rw-r--r--app/uploaders/gitlab_uploader.rb10
-rw-r--r--app/uploaders/job_artifact_uploader.rb9
-rw-r--r--app/uploaders/legacy_artifact_uploader.rb1
-rw-r--r--app/uploaders/lfs_object_uploader.rb6
-rw-r--r--app/uploaders/namespace_file_uploader.rb11
-rw-r--r--app/uploaders/object_storage.rb335
-rw-r--r--app/uploaders/personal_file_uploader.rb17
-rw-r--r--app/uploaders/records_uploads.rb3
-rw-r--r--app/validators/certificate_validator.rb2
-rw-r--r--app/validators/importable_url_validator.rb2
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml39
-rw-r--r--app/views/admin/application_settings/_form.html.haml257
-rw-r--r--app/views/admin/application_settings/_help_page.html.haml22
-rw-r--r--app/views/admin/application_settings/_pages.html.haml22
-rw-r--r--app/views/admin/application_settings/_signin.html.haml59
-rw-r--r--app/views/admin/application_settings/_signup.html.haml58
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml66
-rw-r--r--app/views/admin/application_settings/show.html.haml74
-rw-r--r--app/views/ci/variables/_variable_row.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml6
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/layouts/header/_read_only_banner.html.haml7
-rw-r--r--app/views/notify/push_to_merge_request_email.html.haml26
-rw-r--r--app/views/notify/push_to_merge_request_email.text.haml13
-rw-r--r--app/views/projects/_last_push.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml4
-rw-r--r--app/views/projects/branches/_branch.html.haml141
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml2
-rw-r--r--app/views/projects/pages/_https_only.html.haml10
-rw-r--r--app/views/projects/pages/show.html.haml3
-rw-r--r--app/workers/all_queues.yml4
-rw-r--r--app/workers/concerns/object_storage_queue.rb8
-rw-r--r--app/workers/object_storage/background_move_worker.rb29
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb202
-rw-r--r--app/workers/object_storage_upload_worker.rb21
130 files changed, 1897 insertions, 759 deletions
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index 7e882a57202..8aee5b23c76 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
@@ -45,7 +45,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
};
},
components: {
- userAvatarLink,
+ UserAvatarLink,
},
computed: {
numberOverLimit() {
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index 745f3404295..e177a3bfdc7 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -33,7 +33,7 @@ export default class VariableList {
selector: '.js-ci-variable-input-key',
default: '',
},
- value: {
+ secret_value: {
selector: '.js-ci-variable-input-value',
default: '',
},
@@ -105,7 +105,7 @@ export default class VariableList {
setupToggleButtons($row[0]);
// Reset the resizable textarea
- $row.find(this.inputMap.value.selector).css('height', '');
+ $row.find(this.inputMap.secret_value.selector).css('height', '');
const $environmentSelect = $row.find('.js-variable-environment-toggle');
if ($environmentSelect.length) {
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 46232726510..d62d3c23654 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -1,4 +1,5 @@
// ECMAScript polyfills
+import 'core-js/fn/array/fill';
import 'core-js/fn/array/find';
import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from';
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 63bb5832bd0..22eb7bd44c5 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -4,7 +4,7 @@
import $ from 'jquery';
import { s__ } from '~/locale';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
-import modal from '~/vue_shared/components/modal.vue';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -15,7 +15,7 @@ import groupsComponent from './groups.vue';
export default {
components: {
loadingIcon,
- modal,
+ DeprecatedModal,
groupsComponent,
},
props: {
@@ -52,8 +52,9 @@ export default {
},
},
created() {
- this.searchEmptyMessage = this.hideProjects ?
- COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
+ this.searchEmptyMessage = this.hideProjects
+ ? COMMON_STR.GROUP_SEARCH_EMPTY
+ : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleChildren', this.toggleChildren);
@@ -72,22 +73,30 @@ export default {
eventHub.$off('updateGroups', this.updateGroups);
},
methods: {
- fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
- return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
- .then((res) => {
- if (updatePagination) {
- this.updatePagination(res.headers);
- }
+ fetchGroups({
+ parentId,
+ page,
+ filterGroupsBy,
+ sortBy,
+ archived,
+ updatePagination,
+ }) {
+ return this.service
+ .getGroups(parentId, page, filterGroupsBy, sortBy, archived)
+ .then(res => {
+ if (updatePagination) {
+ this.updatePagination(res.headers);
+ }
- return res;
- })
- .then(res => res.json())
- .catch(() => {
- this.isLoading = false;
- $.scrollTo(0);
+ return res;
+ })
+ .then(res => res.json())
+ .catch(() => {
+ this.isLoading = false;
+ $.scrollTo(0);
- Flash(COMMON_STR.FAILURE);
- });
+ Flash(COMMON_STR.FAILURE);
+ });
},
fetchAllGroups() {
const page = getParameterByName('page') || null;
@@ -103,7 +112,7 @@ export default {
sortBy,
archived,
updatePagination: true,
- }).then((res) => {
+ }).then(res => {
this.isLoading = false;
this.updateGroups(res, Boolean(filterGroupsBy));
});
@@ -118,14 +127,18 @@ export default {
sortBy,
archived,
updatePagination: true,
- }).then((res) => {
+ }).then(res => {
this.isLoading = false;
$.scrollTo(0);
const currentPath = mergeUrlParams({ page }, window.location.href);
- window.history.replaceState({
- page: currentPath,
- }, document.title, currentPath);
+ window.history.replaceState(
+ {
+ page: currentPath,
+ },
+ document.title,
+ currentPath,
+ );
this.updateGroups(res);
});
@@ -138,11 +151,13 @@ export default {
// eslint-disable-next-line promise/catch-or-return
this.fetchGroups({
parentId: parentGroup.id,
- }).then((res) => {
- this.store.setGroupChildren(parentGroup, res);
- }).catch(() => {
- parentGroup.isChildrenLoading = false;
- });
+ })
+ .then(res => {
+ this.store.setGroupChildren(parentGroup, res);
+ })
+ .catch(() => {
+ parentGroup.isChildrenLoading = false;
+ });
} else {
parentGroup.isOpen = true;
}
@@ -154,7 +169,11 @@ export default {
this.targetGroup = group;
this.targetParentGroup = parentGroup;
this.showModal = true;
- this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`);
+ this.groupLeaveConfirmationMessage = s__(
+ `GroupsTree|Are you sure you want to leave the "${
+ group.fullName
+ }" group?`,
+ );
},
hideLeaveGroupModal() {
this.showModal = false;
@@ -162,14 +181,15 @@ export default {
leaveGroup() {
this.showModal = false;
this.targetGroup.isBeingRemoved = true;
- this.service.leaveGroup(this.targetGroup.leavePath)
+ this.service
+ .leaveGroup(this.targetGroup.leavePath)
.then(res => res.json())
- .then((res) => {
+ .then(res => {
$.scrollTo(0);
this.store.removeGroup(this.targetGroup, this.targetParentGroup);
Flash(res.notice, 'notice');
})
- .catch((err) => {
+ .catch(err => {
let message = COMMON_STR.FAILURE;
if (err.status === 403) {
message = COMMON_STR.LEAVE_FORBIDDEN;
@@ -208,8 +228,8 @@ export default {
:search-empty-message="searchEmptyMessage"
:page-info="pageInfo"
/>
- <modal
- v-if="showModal"
+ <deprecated-modal
+ v-show="showModal"
kind="warning"
:primary-button-label="__('Leave')"
:title="__('Are you sure?')"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 5723891d130..4b5a50785b6 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,75 +1,75 @@
<script>
- import { __ } from '~/locale';
- import modal from '~/vue_shared/components/modal.vue';
+import { __ } from '~/locale';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
- export default {
- components: {
- modal,
+export default {
+ components: {
+ DeprecatedModal,
+ },
+ props: {
+ branchId: {
+ type: String,
+ required: true,
},
- props: {
- branchId: {
- type: String,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- required: true,
- },
+ type: {
+ type: String,
+ required: true,
},
- data() {
- return {
- entryName: this.path !== '' ? `${this.path}/` : '',
- };
+ path: {
+ type: String,
+ required: true,
},
- computed: {
- modalTitle() {
- if (this.type === 'tree') {
- return __('Create new directory');
- }
+ },
+ data() {
+ return {
+ entryName: this.path !== '' ? `${this.path}/` : '',
+ };
+ },
+ computed: {
+ modalTitle() {
+ if (this.type === 'tree') {
+ return __('Create new directory');
+ }
- return __('Create new file');
- },
- buttonLabel() {
- if (this.type === 'tree') {
- return __('Create directory');
- }
-
- return __('Create file');
- },
- formLabelName() {
- if (this.type === 'tree') {
- return __('Directory name');
- }
+ return __('Create new file');
+ },
+ buttonLabel() {
+ if (this.type === 'tree') {
+ return __('Create directory');
+ }
- return __('File name');
- },
+ return __('Create file');
},
- mounted() {
- this.$refs.fieldName.focus();
+ formLabelName() {
+ if (this.type === 'tree') {
+ return __('Directory name');
+ }
+
+ return __('File name');
},
- methods: {
- createEntryInStore() {
- this.$emit('create', {
- branchId: this.branchId,
- name: this.entryName,
- type: this.type,
- });
+ },
+ mounted() {
+ this.$refs.fieldName.focus();
+ },
+ methods: {
+ createEntryInStore() {
+ this.$emit('create', {
+ branchId: this.branchId,
+ name: this.entryName,
+ type: this.type,
+ });
- this.hideModal();
- },
- hideModal() {
- this.$emit('hide');
- },
+ this.hideModal();
+ },
+ hideModal() {
+ this.$emit('hide');
},
- };
+ },
+};
</script>
<template>
- <modal
+ <deprecated-modal
:title="modalTitle"
:primary-button-label="buttonLabel"
kind="success"
@@ -95,5 +95,5 @@
</div>
</fieldset>
</form>
- </modal>
+ </deprecated-modal>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index caa0f25c827..6e2c3f1ff87 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -2,7 +2,7 @@
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
-import modal from '~/vue_shared/components/modal.vue';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import commitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
@@ -11,7 +11,7 @@ import * as consts from '../stores/modules/commit/constants';
export default {
components: {
- modal,
+ DeprecatedModal,
icon,
commitFilesList,
EmptyState,
@@ -34,11 +34,7 @@ export default {
computed: {
...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters('commit', [
- 'commitButtonDisabled',
- 'discardDraftButtonDisabled',
- 'branchName',
- ]),
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
},
methods: {
...mapActions('commit', [
@@ -48,9 +44,7 @@ export default {
'updateCommitAction',
]),
forceCreateNewBranch() {
- return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
- this.commitChanges(),
- );
+ return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
},
};
@@ -60,7 +54,7 @@ export default {
<div
class="multi-file-commit-panel-section"
>
- <modal
+ <deprecated-modal
id="ide-create-branch-modal"
:primary-button-label="__('Create new branch')"
kind="success"
@@ -71,7 +65,7 @@ export default {
{{ __(`This branch has changed since you started editing.
Would you like to create a new branch?`) }}
</template>
- </modal>
+ </deprecated-modal>
<template
v-if="changedFiles.length || stagedFiles.length"
>
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 38de2fe2b27..887dd7e39b1 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -65,6 +65,10 @@ export default class Editor {
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
readOnly: true,
+ quickSuggestions: false,
+ occurrencesHighlight: false,
+ renderLineHighlight: 'none',
+ hideCursorInOverviewRuler: true,
})),
);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 09f0ea37103..b0573510ff9 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1809,9 +1809,11 @@ export default class Notes {
}
}
+ $closeBtn.text($closeBtn.data('originalText'));
+
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
- axios
+ return axios
.post(`${formAction}?html=true`, formData)
.then(res => {
const note = res.data;
@@ -1928,8 +1930,6 @@ export default class Notes {
this.reenableTargetFormSubmitButton(e);
this.addNoteError($form);
});
-
- return $closeBtn.text($closeBtn.data('originalText'));
}
/**
diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js
new file mode 100644
index 00000000000..48d75f5443b
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/index.js
@@ -0,0 +1,6 @@
+import initSettingsPanels from '~/settings_panels';
+
+document.addEventListener('DOMContentLoaded', () => {
+ // Initialize expandable settings panels
+ initSettingsPanels();
+});
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index 14315d5492e..343c65edb37 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -1,11 +1,11 @@
<script>
import _ from 'underscore';
- import modal from '~/vue_shared/components/modal.vue';
+ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { s__, sprintf } from '~/locale';
export default {
components: {
- modal,
+ DeprecatedModal,
},
props: {
deleteProjectUrl: {
@@ -79,7 +79,7 @@
</script>
<template>
- <modal
+ <deprecated-modal
id="delete-project-modal"
:title="title"
:text="text"
@@ -121,5 +121,5 @@
/>
</form>
</template>
- </modal>
+ </deprecated-modal>
</template>
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 7b5e333011e..0e3ac636661 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -1,11 +1,11 @@
<script>
import _ from 'underscore';
- import modal from '~/vue_shared/components/modal.vue';
+ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { s__, sprintf } from '~/locale';
export default {
components: {
- modal,
+ DeprecatedModal,
},
props: {
deleteUserUrl: {
@@ -113,7 +113,7 @@
</script>
<template>
- <modal
+ <deprecated-modal
id="delete-user-modal"
:title="title"
:text="text"
@@ -170,5 +170,5 @@
{{ secondaryButtonLabel }}
</button>
</template>
- </modal>
+ </deprecated-modal>
</template>
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index c43e0a0490f..16f792d635a 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -2,14 +2,14 @@
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
- import modal from '~/vue_shared/components/modal.vue';
+ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { n__, s__, sprintf } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
export default {
components: {
- modal,
+ DeprecatedModal,
},
props: {
issueCount: {
@@ -92,7 +92,7 @@ Once deleted, it cannot be undone or recovered.`),
</script>
<template>
- <modal
+ <deprecated-modal
id="delete-milestone-modal"
:title="title"
:text="text"
@@ -106,5 +106,5 @@ Once deleted, it cannot be undone or recovered.`),
<p v-html="props.text"></p>
</template>
- </modal>
+ </deprecated-modal>
</template>
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index d4881f07972..db8a0055acd 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -70,6 +70,7 @@ export default {
<td
v-for="key in keys"
:key="key"
+ class="break-word"
>
{{ item[key] }}
</td>
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index d8e792446c3..3ebfaa87a4e 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -1,11 +1,28 @@
+import Vue from 'vue';
+import _ from 'underscore';
import axios from '../../lib/utils/axios_utils';
+let vueResourceInterceptor;
+
export default class PerformanceBarService {
static fetchRequestDetails(peekUrl, requestId) {
return axios.get(peekUrl, { params: { request_id: requestId } });
}
static registerInterceptor(peekUrl, callback) {
+ vueResourceInterceptor = (request, next) => {
+ next(response => {
+ const requestId = response.headers['x-request-id'];
+ const requestUrl = response.url;
+
+ if (requestUrl !== peekUrl && requestId) {
+ callback(requestId, requestUrl);
+ }
+ });
+ };
+
+ Vue.http.interceptors.push(vueResourceInterceptor);
+
return axios.interceptors.response.use(response => {
const requestId = response.headers['x-request-id'];
const requestUrl = response.config.url;
@@ -20,5 +37,9 @@ export default class PerformanceBarService {
static removeInterceptor(interceptor) {
axios.interceptors.response.eject(interceptor);
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors,
+ vueResourceInterceptor,
+ );
}
}
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index c9028952ddd..714aed1333e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -1,5 +1,5 @@
<script>
- import modal from '~/vue_shared/components/modal.vue';
+ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { s__, sprintf } from '~/locale';
import pipelinesTableRowComponent from './pipelines_table_row.vue';
import eventHub from '../event_hub';
@@ -12,7 +12,7 @@
export default {
components: {
pipelinesTableRowComponent,
- modal,
+ DeprecatedModal,
},
props: {
pipelines: {
@@ -120,7 +120,7 @@
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/>
- <modal
+ <deprecated-modal
id="confirmation-modal"
:title="modalTitle"
:text="modalText"
@@ -134,6 +134,6 @@
>
<p v-html="props.text"></p>
</template>
- </modal>
+ </deprecated-modal>
</div>
</template>
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index 1ffe482d782..f50002afbf2 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -1,11 +1,11 @@
<script>
- import modal from '~/vue_shared/components/modal.vue';
+ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { __, s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
export default {
components: {
- modal,
+ DeprecatedModal,
},
props: {
actionUrl: {
@@ -76,7 +76,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
</script>
<template>
- <modal
+ <deprecated-modal
id="delete-account-modal"
:title="s__('Profiles|Delete your account?')"
:text="text"
@@ -131,5 +131,5 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
</form>
</template>
- </modal>
+ </deprecated-modal>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
index a16f9055a6d..95c8b0a4c55 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
@@ -1,4 +1,5 @@
<script>
+import { sprintf, s__ } from '~/locale';
import statusCodes from '../../lib/utils/http_status';
import { bytesToMiB } from '../../lib/utils/number_utils';
import { backOff } from '../../lib/utils/common_utils';
@@ -45,17 +46,28 @@ export default {
shouldShowMetricsUnavailable() {
return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
},
- memoryChangeType() {
+ memoryChangeMessage() {
+ const messageProps = {
+ memoryFrom: this.memoryFrom,
+ memoryTo: this.memoryTo,
+ metricsLinkStart: `<a href="${this.metricsMonitoringUrl}">`,
+ metricsLinkEnd: '</a>',
+ emphasisStart: '<b>',
+ emphasisEnd: '</b>',
+ };
const memoryTo = Number(this.memoryTo);
const memoryFrom = Number(this.memoryFrom);
+ let memoryUsageMsg = '';
if (memoryTo > memoryFrom) {
- return 'increased';
+ memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB'), messageProps, false);
} else if (memoryTo < memoryFrom) {
- return 'decreased';
+ memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB'), messageProps, false);
+ } else {
+ memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB'), messageProps, false);
}
- return 'unchanged';
+ return memoryUsageMsg;
},
},
mounted() {
@@ -130,24 +142,22 @@ export default {
<i
class="fa fa-spinner fa-spin usage-info-load-spinner"
aria-hidden="true">
- </i>Loading deployment statistics
+ </i>{{ s__('mrWidget|Loading deployment statistics') }}
</p>
<p
v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info">
- <a
- :href="metricsMonitoringUrl"
- >Memory</a> usage <b>{{ memoryChangeType }}</b> from {{ memoryFrom }}MB to {{ memoryTo }}MB
+ {{ memoryChangeMessage }}
</p>
<p
v-if="shouldShowLoadFailure"
class="usage-info js-usage-info usage-info-failed">
- Failed to load deployment statistics
+ {{ s__('mrWidget|Failed to load deployment statistics') }}
</p>
<p
v-if="shouldShowMetricsUnavailable"
class="usage-info js-usage-info usage-info-unavailable">
- Deployment statistics are not available currently
+ {{ s__('mrWidget|Deployment statistics are not available currently') }}
</p>
<memory-graph
v-if="shouldShowMemoryGraph"
diff --git a/app/assets/javascripts/vue_shared/components/modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
index 5f1364421aa..dcf1489b37c 100644
--- a/app/assets/javascripts/vue_shared/components/modal.vue
+++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/require-default-prop */
export default {
- name: 'Modal',
+ name: 'DeprecatedModal', // use GlModal instead
props: {
id: {
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index c35621c9ef3..21ffdc1dc86 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -1,11 +1,11 @@
<script>
- import modal from './modal.vue';
+ import DeprecatedModal from './deprecated_modal.vue';
export default {
name: 'RecaptchaModal',
components: {
- modal,
+ DeprecatedModal,
},
props: {
@@ -65,7 +65,7 @@
</script>
<template>
- <modal
+ <deprecated-modal
kind="warning"
class="recaptcha-modal js-recaptcha-modal"
:hide-footer="true"
@@ -82,5 +82,5 @@
>
</div>
</div>
- </modal>
+ </deprecated-modal>
</template>
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 37d33320445..d0dda50a835 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -446,6 +446,10 @@ img.emoji {
opacity: .5;
}
+.break-word {
+ word-wrap: break-word;
+}
+
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 6397757bf88..cc74cb72795 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -622,7 +622,7 @@
}
.dropdown-content {
- max-height: $dropdown-max-height;
+ max-height: 252px;
overflow-y: auto;
}
@@ -699,6 +699,31 @@
border-radius: $border-radius-base;
}
+.git-revision-dropdown {
+ .dropdown-content {
+ max-height: 215px;
+ }
+}
+
+.sidebar-move-issue-dropdown {
+ .dropdown-content {
+ max-height: 160px;
+ }
+}
+
+.dropdown-menu-author {
+ .dropdown-content {
+ max-height: 215px;
+ }
+}
+
+.dropdown-menu-labels {
+ .dropdown-content {
+ max-height: 128px;
+ }
+}
+
+
.dropdown-menu-due-date {
.dropdown-content {
max-height: 230px;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index d1d98270ad9..3dd4a613789 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -152,3 +152,4 @@
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
+
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index c03d4c2eebf..318d3ddaece 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -31,8 +31,12 @@
.dropdown-menu-issues-board-new {
width: 320px;
+ .open & {
+ max-height: 400px;
+ }
+
.dropdown-content {
- max-height: 150px;
+ max-height: 162px;
}
}
diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss
index 3e2fa8ca88d..49fe50977f5 100644
--- a/app/assets/stylesheets/pages/branches.scss
+++ b/app/assets/stylesheets/pages/branches.scss
@@ -1,6 +1,17 @@
+.content-list > .branch-item,
+.branch-title {
+ display: flex;
+ align-items: center;
+}
+
+.branch-info {
+ flex: auto;
+ min-width: 0;
+ overflow: hidden;
+}
+
.divergence-graph {
- padding: 12px 12px 0 0;
- float: right;
+ padding: 0 6px;
.graph-side {
position: relative;
@@ -53,3 +64,9 @@
background-color: $divergence-graph-separator-bg;
}
}
+
+.divergence-graph,
+.branch-item .controls {
+ flex: 0 0 auto;
+ white-space: nowrap;
+}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 8871a069d5d..d9267f5cdf3 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -162,17 +162,14 @@
* Last push widget
*/
.event-last-push {
- overflow: auto;
width: 100%;
+ display: flex;
+ align-items: center;
.event-last-push-text {
@include str-truncated(100%);
- padding: 4px 0;
font-size: 13px;
- float: left;
- margin-right: -150px;
- padding-right: 150px;
- line-height: 20px;
+ margin-right: $gl-padding;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 0f49d15203b..b0852adb459 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -26,9 +26,15 @@
}
}
+.dropdown-menu-labels {
+ .dropdown-content {
+ max-height: 135px;
+ }
+}
+
.dropdown-new-label {
.dropdown-content {
- max-height: 260px;
+ max-height: 136px;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 85de0d8e70f..584b0579b72 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -9,7 +9,6 @@
.new_project,
.edit-project,
.import-project {
-
.help-block {
margin-bottom: 10px;
}
@@ -18,18 +17,25 @@
border-radius: $border-radius-base;
}
- .input-group > div {
+ .input-group {
+ display: flex;
- &:last-child {
- padding-right: 0;
+ .select2-container {
+ display: unset;
+ max-width: unset;
+ width: unset !important;
+ flex-grow: 1;
+ }
+
+ > div {
+ &:last-child {
+ padding-right: 0;
+ }
}
}
@media (max-width: $screen-xs-max) {
.input-group > div {
-
- margin-bottom: 14px;
-
&:last-child {
margin-bottom: 0;
}
@@ -41,17 +47,24 @@
}
.input-group-addon {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: unset;
+ width: unset;
+ max-width: 50%;
+ text-align: left;
&.static-namespace {
height: 35px;
border-radius: 3px;
border: 1px solid $border-color;
+ max-width: 100%;
+ flex-grow: 1;
}
+ .select2 a,
+ .btn-default {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
+ border-radius: 0 $border-radius-base $border-radius-base 0;
}
}
}
@@ -290,7 +303,7 @@
font-size: 13px;
font-weight: $gl-font-weight-bold;
line-height: 13px;
- letter-spacing: .4px;
+ letter-spacing: 0.4px;
padding: 6px 14px;
text-align: center;
vertical-align: middle;
@@ -443,7 +456,7 @@ a.deploy-project-label {
text-decoration: none;
&.disabled {
- opacity: .3;
+ opacity: 0.3;
cursor: not-allowed;
}
}
@@ -600,26 +613,26 @@ a.deploy-project-label {
}
.first-column {
- @media(min-width: $screen-xs-min) {
+ @media (min-width: $screen-xs-min) {
max-width: 50%;
padding-right: 30px;
}
- @media(max-width: $screen-xs-max) {
+ @media (max-width: $screen-xs-max) {
max-width: 100%;
width: 100%;
}
}
.second-column {
- @media(min-width: $screen-xs-min) {
+ @media (min-width: $screen-xs-min) {
width: 50%;
flex: 1;
padding-left: 30px;
position: relative;
}
- @media(max-width: $screen-xs-max) {
+ @media (max-width: $screen-xs-max) {
max-width: 100%;
width: 100%;
padding-left: 0;
@@ -632,7 +645,7 @@ a.deploy-project-label {
}
&::before {
- content: "OR";
+ content: 'OR';
position: absolute;
left: -10px;
top: 50%;
@@ -656,7 +669,7 @@ a.deploy-project-label {
}
&::after {
- content: "";
+ content: '';
position: absolute;
background-color: $border-color;
bottom: 0;
@@ -921,10 +934,7 @@ pre.light-well {
border-right: solid 1px transparent;
}
}
-}
-.protected-tags-list,
-.protected-branches-list {
.dropdown-menu-toggle {
width: 100%;
max-width: 300px;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 983a3465ee2..79cf93ee607 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -294,6 +294,10 @@
.margin-view-overlays .delete-sign {
opacity: 0.4;
}
+
+ .cursors-layer {
+ display: none;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index c9363188505..dbde0720993 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -112,7 +112,7 @@ input[type="checkbox"]:hover {
}
.dropdown-content {
- max-height: 350px;
+ max-height: 302px;
}
}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 5d1a9489aad..45ae94abaff 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -15,6 +15,10 @@
line-height: $performance-bar-height;
color: $perf-bar-text;
+ select {
+ width: 200px;
+ }
+
&.disabled {
display: none;
}
@@ -102,8 +106,14 @@
}
}
- .performance-bar-modal .modal-footer {
- display: none;
+ .performance-bar-modal {
+ .modal-footer {
+ display: none;
+ }
+
+ .modal-dialog {
+ width: 860px;
+ }
}
}
diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb
index c27f2ee3c09..a4648b33cfa 100644
--- a/app/controllers/admin/application_controller.rb
+++ b/app/controllers/admin/application_controller.rb
@@ -3,23 +3,9 @@
# Automatically sets the layout and ensures an administrator is logged in
class Admin::ApplicationController < ApplicationController
before_action :authenticate_admin!
- before_action :display_read_only_information
layout 'admin'
def authenticate_admin!
render_404 unless current_user.admin?
end
-
- def display_read_only_information
- return unless Gitlab::Database.read_only?
-
- flash.now[:notice] = read_only_message
- end
-
- private
-
- # Overridden in EE
- def read_only_message
- _('You are on a read-only GitLab instance.')
- end
end
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
new file mode 100644
index 00000000000..55011c89886
--- /dev/null
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -0,0 +1,17 @@
+module SendFileUpload
+ def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment')
+ if attachment
+ redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" }
+ send_params.merge!(filename: attachment, disposition: disposition)
+ end
+
+ if file_upload.file_storage?
+ send_file file_upload.path, send_params
+ elsif file_upload.class.proxy_download_enabled?
+ headers.store(*Gitlab::Workhorse.send_url(file_upload.url(**redirect_params)))
+ head :ok
+ else
+ redirect_to file_upload.url(**redirect_params)
+ end
+ end
+end
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index 3dbfabcae8a..b9b9b6e4e88 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -1,5 +1,6 @@
module UploadsActions
include Gitlab::Utils::StrongMemoize
+ include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze
@@ -26,14 +27,11 @@ module UploadsActions
def show
return render_404 unless uploader&.exists?
- if uploader.file_storage?
- disposition = uploader.image_or_video? ? 'inline' : 'attachment'
- expires_in 0.seconds, must_revalidate: true, private: true
+ expires_in 0.seconds, must_revalidate: true, private: true
- send_file uploader.file.path, disposition: disposition
- else
- redirect_to uploader.url
- end
+ disposition = uploader.image_or_video? ? 'inline' : 'attachment'
+
+ send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end
private
@@ -62,19 +60,27 @@ module UploadsActions
end
def build_uploader_from_upload
- return nil unless params[:secret] && params[:filename]
+ return unless uploader = build_uploader
- upload_path = uploader_class.upload_path(params[:secret], params[:filename])
- upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_path)
+ upload_paths = uploader.upload_paths(params[:filename])
+ upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_paths)
upload&.build_uploader
end
def build_uploader_from_params
+ return unless uploader = build_uploader
+
+ uploader.retrieve_from_store!(params[:filename])
+ uploader
+ end
+
+ def build_uploader
+ return unless params[:secret] && params[:filename]
+
uploader = uploader_class.new(model, secret: params[:secret])
- return nil unless uploader.model_valid?
+ return unless uploader.model_valid?
- uploader.retrieve_from_store!(params[:filename])
uploader
end
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index cb8771bc97e..6142e75b4c1 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -39,7 +39,7 @@ module Groups
end
def variable_params_attributes
- %i[id key value protected _destroy]
+ %i[id key secret_value protected _destroy]
end
def authorize_admin_build!
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 8440945ab43..5e6676ea513 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -18,6 +18,18 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
+ # Extend the standard implementation to also increment
+ # the number of failed sign in attempts
+ def failure
+ if params[:username].present? && AuthHelper.form_based_provider?(failed_strategy.name)
+ user = User.by_login(params[:username])
+
+ user&.increment_failed_attempts!
+ end
+
+ super
+ end
+
# Extend the standard message generation to accept our custom exception
def failure_message
exception = env["omniauth.error"]
@@ -95,6 +107,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
handle_omniauth
end
+ def auth0
+ if oauth['uid'].blank?
+ fail_auth0_login
+ else
+ handle_omniauth
+ end
+ end
+
private
def handle_omniauth
@@ -170,6 +190,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to new_user_session_path
end
+ def fail_auth0_login
+ flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
+
+ redirect_to new_user_session_path
+ end
+
def handle_disabled_provider
label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
flash[:alert] = "Signing in using #{label} has been disabled"
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 0837451cc49..abc283d7aa9 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -1,6 +1,7 @@
class Projects::ArtifactsController < Projects::ApplicationController
include ExtractsPath
include RendersBlob
+ include SendFileUpload
layout 'project'
before_action :authorize_read_build!
@@ -10,11 +11,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :entry, only: [:file]
def download
- if artifacts_file.file_storage?
- send_file artifacts_file.path, disposition: 'attachment'
- else
- redirect_to artifacts_file.url
- end
+ send_upload(artifacts_file, attachment: artifacts_file.filename)
end
def browse
@@ -45,8 +42,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def raw
- path = Gitlab::Ci::Build::Artifacts::Path
- .new(params[:path])
+ path = Gitlab::Ci::Build::Artifacts::Path.new(params[:path])
send_artifacts_entry(build, path)
end
@@ -75,7 +71,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def validate_artifacts!
- render_404 unless build && build.artifacts?
+ render_404 unless build&.artifacts?
end
def build
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 8b54ba3ad7c..85e972d9731 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -1,4 +1,6 @@
class Projects::JobsController < Projects::ApplicationController
+ include SendFileUpload
+
before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!,
@@ -117,11 +119,17 @@ class Projects::JobsController < Projects::ApplicationController
end
def raw
- build.trace.read do |stream|
- if stream.file?
- send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
- else
- render_404
+ if trace_artifact_file
+ send_upload(trace_artifact_file,
+ send_params: raw_send_params,
+ redirect_params: raw_redirect_params)
+ else
+ build.trace.read do |stream|
+ if stream.file?
+ send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
+ else
+ render_404
+ end
end
end
end
@@ -136,9 +144,21 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :erase_build, build)
end
+ def raw_send_params
+ { type: 'text/plain; charset=utf-8', disposition: 'inline' }
+ end
+
+ def raw_redirect_params
+ { query: { 'response-content-type' => 'text/plain; charset=utf-8', 'response-content-disposition' => 'inline' } }
+ end
+
+ def trace_artifact_file
+ @trace_artifact_file ||= build.job_artifacts_trace&.file
+ end
+
def build
@build ||= project.builds.find(params[:id])
- .present(current_user: current_user)
+ .present(current_user: current_user)
end
def build_path(build)
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 941638db427..6b16f1ccbbb 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -1,6 +1,7 @@
class Projects::LfsStorageController < Projects::GitHttpClientController
include LfsRequest
include WorkhorseRequest
+ include SendFileUpload
skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize]
@@ -11,7 +12,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
return
end
- send_file lfs_object.file.path, content_type: "application/octet-stream"
+ send_upload(lfs_object.file, send_params: { content_type: "application/octet-stream" })
end
def upload_authorize
@@ -70,10 +71,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
end
def move_tmp_file_to_storage(object, path)
- File.open(path) do |f|
- object.file = f
- end
-
+ object.file = File.open(path)
object.file.store!
object.save
end
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index d421b1a8eb5..cae6e2c40b8 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -21,4 +21,26 @@ class Projects::PagesController < Projects::ApplicationController
end
end
end
+
+ def update
+ result = Projects::UpdateService.new(@project, current_user, project_params).execute
+
+ respond_to do |format|
+ format.html do
+ if result[:status] == :success
+ flash[:notice] = 'Your changes have been saved'
+ else
+ flash[:alert] = 'Something went wrong on our end'
+ end
+
+ redirect_to project_pages_path(@project)
+ end
+ end
+ end
+
+ private
+
+ def project_params
+ params.require(:project).permit(:pages_https_only)
+ end
end
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index b478e7b5e05..fa258f3d9af 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -92,7 +92,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def schedule_params
params.require(:schedule)
.permit(:description, :cron, :cron_timezone, :ref, :active,
- variables_attributes: [:id, :key, :value, :_destroy] )
+ variables_attributes: [:id, :key, :secret_value, :_destroy] )
end
def authorize_play_pipeline_schedule!
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 06ce7328fb5..557671ab186 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -10,10 +10,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
if service.execute
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
- if service.run_auto_devops_pipeline?
- CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
- flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
- end
+ run_autodevops_pipeline(service)
redirect_to project_settings_ci_cd_path(@project)
else
@@ -24,6 +21,18 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
private
+ def run_autodevops_pipeline(service)
+ return unless service.run_auto_devops_pipeline?
+
+ if @project.empty_repo?
+ flash[:warning] = "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch."
+ return
+ end
+
+ CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
+ flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
+ end
+
def update_params
params.require(:project).permit(
:runners_token, :builds_enabled, :build_allow_git_fetch,
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index a02cc477e08..9bc774b7636 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -2,6 +2,7 @@
class Projects::RawController < Projects::ApplicationController
include ExtractsPath
include BlobHelper
+ include SendFileUpload
before_action :require_non_empty_project
before_action :assign_ref_vars
@@ -31,7 +32,7 @@ class Projects::RawController < Projects::ApplicationController
lfs_object = find_lfs_object
if lfs_object && lfs_object.project_allowed_access?(@project)
- send_file lfs_object.file.path, filename: @blob.name, disposition: 'attachment'
+ send_upload(lfs_object.file, attachment: @blob.name)
else
render_404
end
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 7eb509e2e64..517d0b026c2 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -36,6 +36,6 @@ class Projects::VariablesController < Projects::ApplicationController
end
def variable_params_attributes
- %i[id key value protected _destroy]
+ %i[id key secret_value protected _destroy]
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 3ddf8eb3369..701be97ee96 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -323,4 +323,11 @@ module ApplicationHelper
def locale_path
asset_path("locale/#{Gitlab::I18n.locale}/app.js")
end
+
+ # Overridden in EE
+ def read_only_message
+ return unless Gitlab::Database.read_only?
+
+ _('You are on a read-only GitLab instance.')
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 4c4d7cca8a5..b3b080e6dcf 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -96,7 +96,7 @@ module ApplicationSettingsHelper
def repository_storages_options_for_select(selected)
options = Gitlab.config.repositories.storages.map do |name, storage|
- ["#{name} - #{storage['path']}", name]
+ ["#{name} - #{storage['gitaly_address']}", name]
end
options_for_select(options, selected)
@@ -245,7 +245,8 @@ module ApplicationSettingsHelper
:usage_ping_enabled,
:user_default_external,
:user_oauth_applications,
- :version_check_enabled
+ :version_check_enabled,
+ :allow_local_requests_from_hooks_and_services
]
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index da9fe734f1c..15f48e43a28 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -531,4 +531,22 @@ module ProjectsHelper
def can_show_last_commit_in_list?(project)
can?(current_user, :read_cross_project) && project.commit
end
+
+ def pages_https_only_disabled?
+ !@project.pages_domains.all?(&:https?)
+ end
+
+ def pages_https_only_title
+ return unless pages_https_only_disabled?
+
+ "You must enable HTTPS for all your domains first"
+ end
+
+ def pages_https_only_label_class
+ if pages_https_only_disabled?
+ "list-label disabled"
+ else
+ "list-label"
+ end
+ end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 5fe09cea83f..be99f3780cc 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -11,6 +11,14 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
+ def push_to_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil, new_commits: [], existing_commits: [])
+ setup_merge_request_mail(merge_request_id, recipient_id)
+ @new_commits = new_commits
+ @existing_commits = existing_commits
+
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
+ end
+
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index dcd14c08f3c..2a6406d63c7 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -1,5 +1,7 @@
class Appearance < ActiveRecord::Base
include CacheMarkdownField
+ include AfterCommitQueue
+ include ObjectStorage::BackgroundMove
cache_markdown_field :description
cache_markdown_field :new_project_guidelines
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 3cbbf8b5dfa..862933bf127 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -330,7 +330,8 @@ class ApplicationSetting < ActiveRecord::Base
usage_ping_enabled: Settings.gitlab['usage_ping_enabled'],
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
- gitaly_timeout_default: 55
+ gitaly_timeout_default: 55,
+ allow_local_requests_from_hooks_and_services: false
}
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1e066b69c6e..08bb5915d10 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -3,6 +3,7 @@ module Ci
prepend ArtifactMigratable
include TokenAuthenticatable
include AfterCommitQueue
+ include ObjectStorage::BackgroundMove
include Presentable
include Importable
@@ -45,6 +46,7 @@ module Ci
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end
+ scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
@@ -365,13 +367,19 @@ module Ci
project.running_or_pending_build_count(force: true)
end
+ def browsable_artifacts?
+ artifacts_metadata?
+ end
+
def artifacts_metadata_entry(path, **options)
- metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
- artifacts_metadata.path,
- path,
- **options)
+ artifacts_metadata.use_file do |metadata_path|
+ metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
+ metadata_path,
+ path,
+ **options)
- metadata.to_entry
+ metadata.to_entry
+ end
end
def erase_artifacts!
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 1dd0e050ba9..62d768cc6cf 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -6,6 +6,8 @@ module Ci
belongs_to :group
+ alias_attribute :secret_value, :value
+
validates :key, uniqueness: {
scope: :group_id,
message: "(%{value}) has already been taken"
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 0a599f72bc7..df57b4f65e3 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -1,5 +1,7 @@
module Ci
class JobArtifact < ActiveRecord::Base
+ include AfterCommitQueue
+ include ObjectStorage::BackgroundMove
extend Gitlab::Ci::Model
belongs_to :project
@@ -7,9 +9,11 @@ module Ci
before_save :set_size, if: :file_changed?
+ scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
+
mount_uploader :file, JobArtifactUploader
- delegate :open, :exists?, to: :file
+ delegate :exists?, :open, to: :file
enum file_type: {
archive: 1,
@@ -21,6 +25,10 @@ module Ci
self.where(project: project).sum(:size)
end
+ def local_store?
+ [nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store)
+ end
+
def set_size
self.size = file.size
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index f2edcdd61fd..434b9b64c65 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -6,6 +6,7 @@ module Ci
include AfterCommitQueue
include Presentable
include Gitlab::OptimisticLocking
+ include Gitlab::Utils::StrongMemoize
belongs_to :project, inverse_of: :pipelines
belongs_to :user
@@ -14,7 +15,7 @@ module Ci
has_many :stages
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
- has_many :builds, foreign_key: :commit_id
+ has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
@@ -361,21 +362,23 @@ module Ci
def stage_seeds
return [] unless config_processor
- @stage_seeds ||= config_processor.stage_seeds(self)
+ strong_memoize(:stage_seeds) do
+ seeds = config_processor.stages_attributes.map do |attributes|
+ Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes)
+ end
+
+ seeds.select(&:included?)
+ end
end
def seeds_size
- @seeds_size ||= stage_seeds.sum(&:size)
+ stage_seeds.sum(&:size)
end
def has_kubernetes_active?
project.deployment_platform&.active?
end
- def has_stage_seeds?
- stage_seeds.any?
- end
-
def has_warnings?
builds.latest.failed_but_allowed.any?
end
@@ -388,6 +391,9 @@ module Ci
end
end
+ ##
+ # TODO, setting yaml_errors should be moved to the pipeline creation chain.
+ #
def config_processor
return unless ci_yaml_file
return @config_processor if defined?(@config_processor)
@@ -472,6 +478,14 @@ module Ci
end
end
+ def protected_ref?
+ strong_memoize(:protected_ref) { project.protected_for?(ref) }
+ end
+
+ def legacy_trigger
+ strong_memoize(:legacy_trigger) { trigger_requests.first }
+ end
+
def predefined_variables
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_PIPELINE_ID', value: id.to_s)
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
index af989fb14b4..03df4e3e638 100644
--- a/app/models/ci/pipeline_schedule_variable.rb
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -5,6 +5,8 @@ module Ci
belongs_to :pipeline_schedule
+ alias_attribute :secret_value, :value
+
validates :key, uniqueness: { scope: :pipeline_schedule_id }
end
end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 7c71291de84..452cb910bca 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -6,6 +6,8 @@ module Ci
belongs_to :project
+ alias_attribute :secret_value, :value
+
validates :key, uniqueness: {
scope: [:project_id, :environment_scope],
message: "(%{value}) has already been taken"
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 49eb069016a..bfdfc5ae6fe 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -10,6 +10,7 @@ module Clusters
Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner
}.freeze
+ DEFAULT_ENVIRONMENT = '*'.freeze
belongs_to :user
@@ -50,6 +51,7 @@ module Clusters
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
+ scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
def status_name
if provider
diff --git a/app/models/commit.rb b/app/models/commit.rb
index cceae5efb72..b64462fb768 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -175,7 +175,7 @@ class Commit
if safe_message.blank?
no_commit_message
else
- safe_message.split("\n", 2).first
+ safe_message.split(/[\r\n]/, 2).first
end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index d35e37935fb..7677891b9ce 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -3,6 +3,7 @@ module Avatarable
included do
prepend ShadowMethods
+ include ObjectStorage::BackgroundMove
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
@@ -21,7 +22,7 @@ module Avatarable
def avatar_type
unless self.avatar.image?
- self.errors.add :avatar, "only images allowed"
+ errors.add :avatar, "file format is not supported. Please try one of the following supported formats: #{AvatarUploader::IMAGE_EXT.join(', ')}"
end
end
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index faa94204e33..52851b3d0b2 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -1,16 +1,24 @@
module DeploymentPlatform
- # EE would override this and utilize the extra argument
+ # EE would override this and utilize environment argument
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def deployment_platform(environment: nil)
- @deployment_platform ||=
- find_cluster_platform_kubernetes ||
- find_kubernetes_service_integration ||
- build_cluster_and_deployment_platform
+ @deployment_platform ||= {}
+
+ @deployment_platform[environment] ||= find_deployment_platform(environment)
end
private
- def find_cluster_platform_kubernetes
- clusters.find_by(enabled: true)&.platform_kubernetes
+ def find_deployment_platform(environment)
+ find_cluster_platform_kubernetes(environment: environment) ||
+ find_kubernetes_service_integration ||
+ build_cluster_and_deployment_platform
+ end
+
+ # EE would override this and utilize environment argument
+ def find_cluster_platform_kubernetes(environment: nil)
+ clusters.enabled.default_environment
+ .last&.platform_kubernetes
end
def find_kubernetes_service_integration
diff --git a/app/models/event.rb b/app/models/event.rb
index 17a198d52c7..3805f6cf857 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -52,12 +52,12 @@ class Event < ActiveRecord::Base
belongs_to :target, -> {
# If the association for "target" defines an "author" association we want to
# eager-load this so Banzai & friends don't end up performing N+1 queries to
- # get the authors of notes, issues, etc.
- if reflections['events'].active_record.reflect_on_association(:author)
- includes(:author)
- else
- self
+ # get the authors of notes, issues, etc. (likewise for "noteable").
+ incs = %i(author noteable).select do |a|
+ reflections['events'].active_record.reflect_on_association(a)
end
+
+ incs.reduce(self) { |obj, a| obj.includes(a) }
}, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :push_event_payload
diff --git a/app/models/group.rb b/app/models/group.rb
index f669b1a7009..d99af79b5fe 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -189,12 +189,6 @@ class Group < Namespace
owners.include?(user) && owners.size == 1
end
- def avatar_type
- unless self.avatar.image?
- self.errors.add :avatar, "only images allowed"
- end
- end
-
def post_create_hook
Gitlab::AppLogger.info("Group \"#{name}\" was created")
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index b444812a4cf..64e88d5a6a2 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -1,7 +1,12 @@
class LfsObject < ActiveRecord::Base
+ include AfterCommitQueue
+ include ObjectStorage::BackgroundMove
+
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :lfs_objects_projects
+ scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
+
validates :oid, presence: true, uniqueness: true
mount_uploader :file, LfsObjectUploader
@@ -10,6 +15,10 @@ class LfsObject < ActiveRecord::Base
projects.exists?(project.lfs_storage_project.id)
end
+ def local_store?
+ [nil, LfsObjectUploader::Store::LOCAL].include?(self.file_store)
+ end
+
def self.destroy_unreferenced
joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
.where(lfs_objects_projects: { id: nil })
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index e95655e19f8..b3ffad00a07 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -48,7 +48,7 @@ class NotificationRecipient
when :custom
custom_enabled? || %i[participating mention].include?(@type)
when :watch, :participating
- !excluded_watcher_action?
+ !action_excluded?
when :mention
@type == :mention
else
@@ -96,13 +96,22 @@ class NotificationRecipient
end
end
+ def action_excluded?
+ excluded_watcher_action? || excluded_participating_action?
+ end
+
def excluded_watcher_action?
- return false unless @custom_action
- return false if notification_level == :custom
+ return false unless @custom_action && notification_level == :watch
NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
end
+ def excluded_participating_action?
+ return false unless @custom_action && notification_level == :participating
+
+ NotificationSetting::EXCLUDED_PARTICIPATING_EVENTS.include?(@custom_action)
+ end
+
private
def read_ability
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 245f8dddcf9..f6d9b0215fc 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -33,6 +33,7 @@ class NotificationSetting < ActiveRecord::Base
:close_issue,
:reassign_issue,
:new_merge_request,
+ :push_to_merge_request,
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
@@ -41,10 +42,14 @@ class NotificationSetting < ActiveRecord::Base
:success_pipeline
].freeze
- EXCLUDED_WATCHER_EVENTS = [
+ EXCLUDED_PARTICIPATING_EVENTS = [
:success_pipeline
].freeze
+ EXCLUDED_WATCHER_EVENTS = [
+ :push_to_merge_request
+ ].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze
+
def self.find_or_create_for(source)
setting = find_or_initialize_by(source: source)
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 588bd50ed77..2e478a24778 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -6,8 +6,10 @@ class PagesDomain < ActiveRecord::Base
validates :domain, hostname: { allow_numeric_hostname: true }
validates :domain, uniqueness: { case_sensitive: false }
- validates :certificate, certificate: true, allow_nil: true, allow_blank: true
- validates :key, certificate_key: true, allow_nil: true, allow_blank: true
+ validates :certificate, presence: { message: 'must be present if HTTPS-only is enabled' }, if: ->(domain) { domain.project&.pages_https_only? }
+ validates :certificate, certificate: true, if: ->(domain) { domain.certificate.present? }
+ validates :key, presence: { message: 'must be present if HTTPS-only is enabled' }, if: ->(domain) { domain.project&.pages_https_only? }
+ validates :key, certificate_key: true, if: ->(domain) { domain.key.present? }
validates :verification_code, presence: true, allow_blank: false
validate :validate_pages_domain
@@ -46,6 +48,10 @@ class PagesDomain < ActiveRecord::Base
!Gitlab::CurrentSettings.pages_domain_verification_enabled? || enabled_until.present?
end
+ def https?
+ certificate.present?
+ end
+
def to_param
domain
end
diff --git a/app/models/project.rb b/app/models/project.rb
index e5ede967668..6a420663644 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -38,6 +38,9 @@ class Project < ActiveRecord::Base
attachments: 2
}.freeze
+ # Valids ports to import from
+ VALID_IMPORT_PORTS = [22, 80, 443].freeze
+
cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
@@ -264,6 +267,7 @@ class Project < ActiveRecord::Base
validate :visibility_level_allowed_by_group
validate :visibility_level_allowed_as_fork
validate :check_wiki_path_conflict
+ validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
validates :repository_storage,
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
@@ -500,7 +504,7 @@ class Project < ActiveRecord::Base
end
def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
+ Gitlab.config.repositories.storages[repository_storage]&.legacy_disk_path
end
def team
@@ -734,6 +738,26 @@ class Project < ActiveRecord::Base
end
end
+ def pages_https_only
+ return false unless Gitlab.config.pages.external_https
+
+ super
+ end
+
+ def pages_https_only?
+ return false unless Gitlab.config.pages.external_https
+
+ super
+ end
+
+ def validate_pages_https_only
+ return unless pages_https_only?
+
+ unless pages_domains.all?(&:https?)
+ errors.add(:pages_https_only, "cannot be enabled unless all domains have TLS certificates")
+ end
+ end
+
def to_param
if persisted? && errors.include?(:path)
path_was
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index ae6af732ed4..4234b8044e5 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -1,6 +1,4 @@
class AssemblaService < Service
- include HTTParty
-
prop_accessor :token, :subdomain
validates :token, presence: true, if: :activated?
@@ -31,6 +29,6 @@ class AssemblaService < Service
return unless supported_events.include?(data[:object_kind])
url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
- AssemblaService.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
+ Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
end
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 42939ea0ec8..54e4b3278db 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -117,14 +117,14 @@ class BambooService < CiService
url = build_url(path)
if username.blank? && password.blank?
- HTTParty.get(url, verify: false)
+ Gitlab::HTTP.get(url, verify: false)
else
url << '&os_authType=basic'
- HTTParty.get(url, verify: false,
- basic_auth: {
- username: username,
- password: password
- })
+ Gitlab::HTTP.get(url, verify: false,
+ basic_auth: {
+ username: username,
+ password: password
+ })
end
end
end
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index fc30f6e3365..d2aaff8817a 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -71,7 +71,7 @@ class BuildkiteService < CiService
end
def calculate_reactive_cache(sha, ref)
- response = HTTParty.get(commit_status_path(sha), verify: false)
+ response = Gitlab::HTTP.get(commit_status_path(sha), verify: false)
status =
if response.code == 200 && response['status']
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 8d7a4fceb08..cb4af73807b 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -1,6 +1,4 @@
class CampfireService < Service
- include HTTParty
-
prop_accessor :token, :subdomain, :room
validates :token, presence: true, if: :activated?
@@ -31,7 +29,6 @@ class CampfireService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
- self.class.base_uri base_uri
message = build_message(data)
speak(self.room, message, auth)
end
@@ -69,14 +66,14 @@ class CampfireService < Service
}
}
}
- res = self.class.post(path, auth.merge(body))
+ res = Gitlab::HTTP.post(path, base_uri: base_uri, **auth.merge(body))
res.code == 201 ? res : nil
end
# Returns a list of rooms, or [].
# https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
def rooms(auth)
- res = self.class.get("/rooms.json", auth)
+ res = Gitlab::HTTP.get("/rooms.json", base_uri: base_uri, **auth)
res.code == 200 ? res["rooms"] : []
end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index c93f1632652..71b10fc6bc1 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -49,7 +49,7 @@ class DroneCiService < CiService
end
def calculate_reactive_cache(sha, ref)
- response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification)
+ response = Gitlab::HTTP.get(commit_status_path(sha, ref), verify: enable_ssl_verification)
status =
if response.code == 200 && response['status']
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index 720ad61162e..1553f169827 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -1,6 +1,4 @@
class ExternalWikiService < Service
- include HTTParty
-
prop_accessor :external_wiki_url
validates :external_wiki_url, presence: true, url: true, if: :activated?
@@ -24,7 +22,7 @@ class ExternalWikiService < Service
end
def execute(_data)
- @response = HTTParty.get(properties['external_wiki_url'], verify: true) rescue nil
+ @response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true) rescue nil
if @response != 200
nil
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 5fb15c383ca..df6dcd90985 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -77,13 +77,13 @@ class IssueTrackerService < Service
result = false
begin
- response = HTTParty.head(self.project_url, verify: true)
+ response = Gitlab::HTTP.head(self.project_url, verify: true)
if response
message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}"
result = true
end
- rescue HTTParty::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error
+ rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error
message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}"
end
Rails.logger.info(message)
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
index 72ddf9a4be3..2221459c90b 100644
--- a/app/models/project_services/mock_ci_service.rb
+++ b/app/models/project_services/mock_ci_service.rb
@@ -52,7 +52,7 @@ class MockCiService < CiService
#
#
def commit_status(sha, ref)
- response = HTTParty.get(commit_status_path(sha), verify: false)
+ response = Gitlab::HTTP.get(commit_status_path(sha), verify: false)
read_commit_status(response)
rescue Errno::ECONNREFUSED
:error
diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb
index f68a0c1a3c3..ba62a5b7ac0 100644
--- a/app/models/project_services/packagist_service.rb
+++ b/app/models/project_services/packagist_service.rb
@@ -1,6 +1,4 @@
class PackagistService < Service
- include HTTParty
-
prop_accessor :username, :token, :server
validates :username, presence: true, if: :activated?
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index f9dfa2e91c3..3476e7d2283 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -1,6 +1,4 @@
class PivotaltrackerService < Service
- include HTTParty
-
API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'.freeze
prop_accessor :token, :restrict_to_branch
@@ -52,7 +50,7 @@ class PivotaltrackerService < Service
'message' => commit[:message]
}
}
- PivotaltrackerService.post(
+ Gitlab::HTTP.post(
API_ENDPOINT,
body: message.to_json,
headers: {
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index e3a1ca2d45f..8777a44b72f 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -1,6 +1,5 @@
class PushoverService < Service
- include HTTParty
- base_uri 'https://api.pushover.net/1'
+ BASE_URI = 'https://api.pushover.net/1'.freeze
prop_accessor :api_key, :user_key, :device, :priority, :sound
validates :api_key, :user_key, :priority, presence: true, if: :activated?
@@ -99,6 +98,6 @@ class PushoverService < Service
pushover_data[:sound] = sound
end
- PushoverService.post('/messages.json', body: pushover_data)
+ Gitlab::HTTP.post('/messages.json', base_uri: BASE_URI, body: pushover_data)
end
end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index cbe137452bd..145313b8e71 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -83,7 +83,7 @@ class TeamcityService < CiService
branch = Gitlab::Git.ref_name(data[:ref])
- HTTParty.post(
+ Gitlab::HTTP.post(
build_url('httpAuth/app/rest/buildQueue'),
body: "<build branchName=\"#{branch}\">"\
"<buildType id=\"#{build_type}\"/>"\
@@ -134,10 +134,10 @@ class TeamcityService < CiService
end
def get_path(path)
- HTTParty.get(build_url(path), verify: false,
- basic_auth: {
- username: username,
- password: password
- })
+ Gitlab::HTTP.get(build_url(path), verify: false,
+ basic_auth: {
+ username: username,
+ password: password
+ })
end
end
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 99ad37dc892..cf71a7b76fc 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -9,6 +9,8 @@ class Upload < ActiveRecord::Base
validates :model, presence: true
validates :uploader, presence: true
+ scope :with_files_stored_locally, -> { where(store: [nil, ObjectStorage::Store::LOCAL]) }
+
before_save :calculate_checksum!, if: :foreground_checksummable?
after_commit :schedule_checksum, if: :checksummable?
@@ -21,6 +23,7 @@ class Upload < ActiveRecord::Base
end
def absolute_path
+ raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local?
return path unless relative_path?
uploader_class.absolute_path(self)
@@ -30,11 +33,11 @@ class Upload < ActiveRecord::Base
self.checksum = nil
return unless checksummable?
- self.checksum = self.class.hexdigest(absolute_path)
+ self.checksum = Digest::SHA256.file(absolute_path).hexdigest
end
- def build_uploader
- uploader_class.new(model, mount_point, **uploader_context).tap do |uploader|
+ def build_uploader(mounted_as = nil)
+ uploader_class.new(model, mounted_as || mount_point).tap do |uploader|
uploader.upload = self
uploader.retrieve_from_store!(identifier)
end
@@ -51,6 +54,12 @@ class Upload < ActiveRecord::Base
}.compact
end
+ def local?
+ return true if store.nil?
+
+ store == ObjectStorage::Store::LOCAL
+ end
+
private
def delete_file!
@@ -61,10 +70,6 @@ class Upload < ActiveRecord::Base
checksum.nil? && local? && exist?
end
- def local?
- true
- end
-
def foreground_checksummable?
checksummable? && size <= CHECKSUM_THRESHOLD
end
diff --git a/app/models/user.rb b/app/models/user.rb
index b8c55205ab8..fa54581d220 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -623,9 +623,7 @@ class User < ActiveRecord::Base
end
def owned_projects
- @owned_projects ||=
- Project.where('namespace_id IN (?) OR namespace_id = ?',
- owned_groups.select(:id), namespace.id).joins(:namespace)
+ @owned_projects ||= Project.from("(#{owned_projects_union.to_sql}) AS projects")
end
# Returns projects which user can admin issues on (for example to move an issue to that project).
@@ -1196,6 +1194,15 @@ class User < ActiveRecord::Base
private
+ def owned_projects_union
+ Gitlab::SQL::Union.new([
+ Project.where(namespace: namespace),
+ Project.joins(:project_authorizations)
+ .where("projects.namespace_id <> ?", namespace.id)
+ .where(project_authorizations: { user_id: id, access_level: Gitlab::Access::OWNER })
+ ], remove_duplicates: false)
+ end
+
def ci_projects_union
scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
groups = groups_projects.where(members: scope)
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 3b3d9239086..6ce86983287 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -7,6 +7,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip,
+ Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
@@ -65,7 +66,7 @@ module Ci
project.pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.id)
- .where.not(sha: project.repository.sha_from_ref(pipeline.ref))
+ .where.not(sha: project.commit(pipeline.ref).try(:id))
.created_or_pending
end
diff --git a/app/services/ci/create_pipeline_stages_service.rb b/app/services/ci/create_pipeline_stages_service.rb
deleted file mode 100644
index f2c175adee6..00000000000
--- a/app/services/ci/create_pipeline_stages_service.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-module Ci
- class CreatePipelineStagesService < BaseService
- def execute(pipeline)
- pipeline.stage_seeds.each do |seed|
- seed.user = current_user
-
- seed.create! do |build|
- ##
- # Create the environment before the build starts. This sets its slug and
- # makes it available as an environment variable
- #
- if build.has_environment?
- environment_name = build.expanded_environment_name
- project.environments.find_or_create_by(name: environment_name)
- end
- end
- end
- end
- end
-end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index a9813d774bb..85533a1cbdb 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -16,8 +16,8 @@ module Ci
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
.execute(:trigger, ignore_skip_ci: true) do |pipeline|
- pipeline.trigger_requests.create!(trigger: trigger)
- create_pipeline_variables!(pipeline)
+ pipeline.trigger_requests.build(trigger: trigger)
+ pipeline.variables.build(variables)
end
if pipeline.persisted?
@@ -33,14 +33,10 @@ module Ci
end
end
- def create_pipeline_variables!(pipeline)
- return unless params[:variables]
-
- variables = params[:variables].map do |key, value|
+ def variables
+ params[:variables].to_h.map do |key, value|
{ key: key, value: value }
end
-
- pipeline.variables.create!(variables)
end
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 18c40ce8992..1fb1796b56c 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -21,7 +21,7 @@ module MergeRequests
comment_mr_branch_presence_changed
end
- comment_mr_with_commits
+ notify_about_push
mark_mr_as_wip_from_commits
execute_mr_web_hooks
@@ -141,8 +141,8 @@ module MergeRequests
end
end
- # Add comment about pushing new commits to merge requests
- def comment_mr_with_commits
+ # Add comment about pushing new commits to merge requests and send nofitication emails
+ def notify_about_push
return unless @commits.present?
merge_requests_for_source_branch.each do |merge_request|
@@ -155,6 +155,8 @@ module MergeRequests
SystemNoteService.add_commits(merge_request, merge_request.project,
@current_user, new_commits,
existing_commits, @oldrev)
+
+ notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits)
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index d7d2cde1004..f94c76cf3ac 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -113,6 +113,16 @@ class NotificationService
new_resource_email(merge_request, :new_merge_request_email)
end
+ def push_to_merge_request(merge_request, current_user, new_commits: [], existing_commits: [])
+ new_commits = new_commits.map { |c| { short_id: c.short_id, title: c.title } }
+ existing_commits = existing_commits.map { |c| { short_id: c.short_id, title: c.title } }
+ recipients = NotificationRecipientService.build_recipients(merge_request, current_user, action: "push_to")
+
+ recipients.each do |recipient|
+ mailer.send(:push_to_merge_request_email, recipient.user.id, merge_request.id, current_user.id, recipient.reason, new_commits: new_commits, existing_commits: existing_commits).deliver_later
+ end
+ end
+
# When merge request text is updated, we should send an email to:
#
# * newly mentioned project team members with notification level higher than Participating
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index f2d676af5c3..a34024f4f80 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -28,7 +28,7 @@ module Projects
def add_repository_to_project
if project.external_import? && !unknown_url?
- raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
+ raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
end
# We should skip the repository for a GitHub import or GitLab project import,
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
index 52ff64cc938..25017c5cbe3 100644
--- a/app/services/projects/update_pages_configuration_service.rb
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -18,7 +18,8 @@ module Projects
def pages_config
{
- domains: pages_domains_config
+ domains: pages_domains_config,
+ https_only: project.pages_https_only?
}
end
@@ -27,7 +28,8 @@ module Projects
{
domain: domain.domain,
certificate: domain.certificate,
- key: domain.key
+ key: domain.key,
+ https_only: project.pages_https_only? && domain.https?
}
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 00fdd047208..5bf8208e035 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -81,11 +81,13 @@ module Projects
end
def extract_tar_archive!(temp_path)
- results = Open3.pipeline(%W(gunzip -c #{artifacts}),
- %W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
- %W(tar -x -C #{temp_path} #{SITE_PATH}),
- err: '/dev/null')
- raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?)
+ build.artifacts_file.use_file do |artifacts_path|
+ results = Open3.pipeline(%W(gunzip -c #{artifacts_path}),
+ %W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
+ %W(tar -x -C #{temp_path} #{SITE_PATH}),
+ err: '/dev/null')
+ raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?)
+ end
end
def extract_zip_archive!(temp_path)
@@ -103,8 +105,10 @@ module Projects
# -n never overwrite existing files
# We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
site_path = File.join(SITE_PATH, '*')
- unless system(*%W(unzip -qq -n #{artifacts} #{site_path} -d #{temp_path}))
- raise FailedToExtractError, 'pages failed to extract'
+ build.artifacts_file.use_file do |artifacts_path|
+ unless system(*%W(unzip -n #{artifacts_path} #{site_path} -d #{temp_path}))
+ raise FailedToExtractError, 'pages failed to extract'
+ end
end
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 5f2615a2c01..679f4a9cb62 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -24,6 +24,8 @@ module Projects
system_hook_service.execute_hooks_for(project, :update)
end
+ update_pages_config if changing_pages_https_only?
+
success
else
model_errors = project.errors.full_messages.to_sentence
@@ -67,5 +69,13 @@ module Projects
log_error("Could not create wiki for #{project.full_name}")
Gitlab::Metrics.counter(:wiki_can_not_be_created_total, 'Counts the times we failed to create a wiki')
end
+
+ def update_pages_config
+ Projects::UpdatePagesConfigurationService.new(project).execute
+ end
+
+ def changing_pages_https_only?
+ project.previous_changes.include?(:pages_https_only)
+ end
end
end
diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb
index 2623f253d98..ac029fad7ea 100644
--- a/app/services/submit_usage_ping_service.rb
+++ b/app/services/submit_usage_ping_service.rb
@@ -14,16 +14,17 @@ class SubmitUsagePingService
def execute
return false unless Gitlab::CurrentSettings.usage_ping_enabled?
- response = HTTParty.post(
+ response = Gitlab::HTTP.post(
URL,
body: Gitlab::UsageData.to_json(force_refresh: true),
+ allow_local_requests: true,
headers: { 'Content-type' => 'application/json' }
)
store_metrics(response)
true
- rescue HTTParty::Error => e
+ rescue Gitlab::HTTP::Error => e
Rails.logger.info "Unable to contact GitLab, Inc.: #{e}"
false
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 36e589d5aa8..809ce1303d8 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -3,23 +3,20 @@ class WebHookService
attr_reader :body, :headers, :code
def initialize
- @headers = HTTParty::Response::Headers.new({})
+ @headers = Gitlab::HTTP::Response::Headers.new({})
@body = ''
@code = 'internal error'
end
end
- include HTTParty
-
- # HTTParty timeout
- default_timeout Gitlab.config.gitlab.webhook_timeout
-
- attr_accessor :hook, :data, :hook_name
+ attr_accessor :hook, :data, :hook_name, :request_options
def initialize(hook, data, hook_name)
@hook = hook
@data = data
@hook_name = hook_name.to_s
+ @request_options = { timeout: Gitlab.config.gitlab.webhook_timeout }
+ @request_options.merge!(allow_local_requests: true) if @hook.is_a?(SystemHook)
end
def execute
@@ -73,11 +70,12 @@ class WebHookService
end
def make_request(url, basic_auth = false)
- self.class.post(url,
+ Gitlab::HTTP.post(url,
body: data.to_json,
headers: build_headers(hook_name),
verify: hook.enable_ssl_verification,
- basic_auth: basic_auth)
+ basic_auth: basic_auth,
+ **request_options)
end
def make_request_with_auth
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index 4930fb2fca7..cd819dc9bff 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,8 +1,8 @@
class AttachmentUploader < GitlabUploader
- include UploaderHelper
include RecordsUploads::Concern
-
- storage :file
+ include ObjectStorage::Concern
+ prepend ObjectStorage::Extension::RecordsUploads
+ include UploaderHelper
private
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 5c8e1cea62e..5848e6c6994 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,18 +1,18 @@
class AvatarUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern
-
- storage :file
+ include ObjectStorage::Concern
+ prepend ObjectStorage::Extension::RecordsUploads
def exists?
model.avatar.file && model.avatar.file.present?
end
- def move_to_cache
+ def move_to_store
false
end
- def move_to_store
+ def move_to_cache
false
end
diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb
index 8f56f09c9f7..bd7736ad74e 100644
--- a/app/uploaders/file_mover.rb
+++ b/app/uploaders/file_mover.rb
@@ -10,7 +10,11 @@ class FileMover
def execute
move
- uploader.record_upload if update_markdown
+
+ if update_markdown
+ uploader.record_upload
+ uploader.schedule_background_upload
+ end
end
private
@@ -24,11 +28,8 @@ class FileMover
updated_text = model.read_attribute(update_field)
.gsub(temp_file_uploader.markdown_link, uploader.markdown_link)
model.update_attribute(update_field, updated_text)
-
- true
rescue
revert
-
false
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index bde1161dfa8..133fdf6684d 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -9,14 +9,18 @@
class FileUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern
+ include ObjectStorage::Concern
+ prepend ObjectStorage::Extension::RecordsUploads
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
DYNAMIC_PATH_PATTERN = %r{(?<secret>\h{32})/(?<identifier>.*)}
- storage :file
-
after :remove, :prune_store_dir
+ # FileUploader do not run in a model transaction, so we can simply
+ # enqueue a job after the :store hook.
+ after :store, :schedule_background_upload
+
def self.root
File.join(options.storage_path, 'uploads')
end
@@ -28,8 +32,11 @@ class FileUploader < GitlabUploader
)
end
- def self.base_dir(model)
- model_path_segment(model)
+ def self.base_dir(model, store = Store::LOCAL)
+ decorated_model = model
+ decorated_model = Storage::HashedProject.new(model) if store == Store::REMOTE
+
+ model_path_segment(decorated_model)
end
# used in migrations and import/exports
@@ -47,21 +54,24 @@ class FileUploader < GitlabUploader
#
# Returns a String without a trailing slash
def self.model_path_segment(model)
- if model.hashed_storage?(:attachments)
- model.disk_path
+ case model
+ when Storage::HashedProject then model.disk_path
else
- model.full_path
+ model.hashed_storage?(:attachments) ? model.disk_path : model.full_path
end
end
- def self.upload_path(secret, identifier)
- File.join(secret, identifier)
- end
-
def self.generate_secret
SecureRandom.hex
end
+ def upload_paths(filename)
+ [
+ File.join(secret, filename),
+ File.join(base_dir(Store::REMOTE), secret, filename)
+ ]
+ end
+
attr_accessor :model
def initialize(model, mounted_as = nil, **uploader_context)
@@ -71,8 +81,10 @@ class FileUploader < GitlabUploader
apply_context!(uploader_context)
end
- def base_dir
- self.class.base_dir(@model)
+ # enforce the usage of Hashed storage when storing to
+ # remote store as the FileMover doesn't support OS
+ def base_dir(store = nil)
+ self.class.base_dir(@model, store || object_store)
end
# we don't need to know the actual path, an uploader instance should be
@@ -82,15 +94,19 @@ class FileUploader < GitlabUploader
end
def upload_path
- self.class.upload_path(dynamic_segment, identifier)
- end
-
- def model_path_segment
- self.class.model_path_segment(@model)
+ if file_storage?
+ # Legacy path relative to project.full_path
+ File.join(dynamic_segment, identifier)
+ else
+ File.join(store_dir, identifier)
+ end
end
- def store_dir
- File.join(base_dir, dynamic_segment)
+ def store_dirs
+ {
+ Store::LOCAL => File.join(base_dir, dynamic_segment),
+ Store::REMOTE => File.join(base_dir(ObjectStorage::Store::REMOTE), dynamic_segment)
+ }
end
def markdown_link
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 010100f2da1..f12f0466a1d 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -37,12 +37,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
cache_storage.is_a?(CarrierWave::Storage::File)
end
- # Reduce disk IO
def move_to_cache
file_storage?
end
- # Reduce disk IO
def move_to_store
file_storage?
end
@@ -51,10 +49,6 @@ class GitlabUploader < CarrierWave::Uploader::Base
file.present?
end
- def store_dir
- File.join(base_dir, dynamic_segment)
- end
-
def cache_dir
File.join(root, base_dir, 'tmp/cache')
end
@@ -76,6 +70,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
# Designed to be overridden by child uploaders that have a dynamic path
# segment -- that is, a path that changes based on mutable attributes of its
# associated model
+ #
+ # For example, `FileUploader` builds the storage path based on the associated
+ # project model's `path_with_namespace` value, which can change when the
+ # project or its containing namespace is moved or renamed.
def dynamic_segment
raise(NotImplementedError)
end
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index ad5385f45a4..ef0f8acefd6 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -1,5 +1,6 @@
class JobArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
+ include ObjectStorage::Concern
storage_options Gitlab.config.artifacts
@@ -14,9 +15,11 @@ class JobArtifactUploader < GitlabUploader
end
def open
- raise 'Only File System is supported' unless file_storage?
-
- File.open(path, "rb") if path
+ if file_storage?
+ File.open(path, "rb") if path
+ else
+ ::Gitlab::Ci::Trace::HttpIO.new(url, size) if url
+ end
end
private
diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb
index 28c458d3ff1..b726b053493 100644
--- a/app/uploaders/legacy_artifact_uploader.rb
+++ b/app/uploaders/legacy_artifact_uploader.rb
@@ -1,5 +1,6 @@
class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
+ include ObjectStorage::Concern
storage_options Gitlab.config.artifacts
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index e04c97ce179..eb521a22ebc 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -1,10 +1,6 @@
class LfsObjectUploader < GitlabUploader
extend Workhorse::UploadPath
-
- # LfsObject are in `tmp/upload` instead of `tmp/uploads`
- def self.workhorse_upload_path
- File.join(root, 'tmp/upload')
- end
+ include ObjectStorage::Concern
storage_options Gitlab.config.lfs
diff --git a/app/uploaders/namespace_file_uploader.rb b/app/uploaders/namespace_file_uploader.rb
index 993e85fbc13..1085ecb1700 100644
--- a/app/uploaders/namespace_file_uploader.rb
+++ b/app/uploaders/namespace_file_uploader.rb
@@ -4,7 +4,7 @@ class NamespaceFileUploader < FileUploader
options.storage_path
end
- def self.base_dir(model)
+ def self.base_dir(model, _store = nil)
File.join(options.base_dir, 'namespace', model_path_segment(model))
end
@@ -14,6 +14,13 @@ class NamespaceFileUploader < FileUploader
# Re-Override
def store_dir
- File.join(base_dir, dynamic_segment)
+ store_dirs[object_store]
+ end
+
+ def store_dirs
+ {
+ Store::LOCAL => File.join(base_dir, dynamic_segment),
+ Store::REMOTE => File.join('namespace', self.class.model_path_segment(model), dynamic_segment)
+ }
end
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
new file mode 100644
index 00000000000..7218cb0a0fc
--- /dev/null
+++ b/app/uploaders/object_storage.rb
@@ -0,0 +1,335 @@
+require 'fog/aws'
+require 'carrierwave/storage/fog'
+
+#
+# This concern should add object storage support
+# to the GitlabUploader class
+#
+module ObjectStorage
+ RemoteStoreError = Class.new(StandardError)
+ UnknownStoreError = Class.new(StandardError)
+ ObjectStorageUnavailable = Class.new(StandardError)
+
+ module Store
+ LOCAL = 1
+ REMOTE = 2
+ end
+
+ module Extension
+ # this extension is the glue between the ObjectStorage::Concern and RecordsUploads::Concern
+ module RecordsUploads
+ extend ActiveSupport::Concern
+
+ def prepended(base)
+ raise "#{base} must include ObjectStorage::Concern to use extensions." unless base < Concern
+
+ base.include(RecordsUploads::Concern)
+ end
+
+ def retrieve_from_store!(identifier)
+ paths = store_dirs.map { |store, path| File.join(path, identifier) }
+
+ unless current_upload_satisfies?(paths, model)
+ # the upload we already have isn't right, find the correct one
+ self.upload = uploads.find_by(model: model, path: paths)
+ end
+
+ super
+ end
+
+ def build_upload
+ super.tap do |upload|
+ upload.store = object_store
+ end
+ end
+
+ def upload=(upload)
+ return unless upload
+
+ self.object_store = upload.store
+ super
+ end
+
+ def schedule_background_upload(*args)
+ return unless schedule_background_upload?
+ return unless upload
+
+ ObjectStorage::BackgroundMoveWorker.perform_async(self.class.name,
+ upload.class.to_s,
+ mounted_as,
+ upload.id)
+ end
+
+ private
+
+ def current_upload_satisfies?(paths, model)
+ return false unless upload
+ return false unless model
+
+ paths.include?(upload.path) &&
+ upload.model_id == model.id &&
+ upload.model_type == model.class.base_class.sti_name
+ end
+ end
+ end
+
+ # Add support for automatic background uploading after the file is stored.
+ #
+ module BackgroundMove
+ extend ActiveSupport::Concern
+
+ def background_upload(mount_points = [])
+ return unless mount_points.any?
+
+ run_after_commit do
+ mount_points.each { |mount| send(mount).schedule_background_upload } # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def changed_mounts
+ self.class.uploaders.select do |mount, uploader_class|
+ mounted_as = uploader_class.serialization_column(self.class, mount)
+ uploader = send(:"#{mounted_as}") # rubocop:disable GitlabSecurity/PublicSend
+
+ next unless uploader
+ next unless uploader.exists?
+ next unless send(:"#{mounted_as}_changed?") # rubocop:disable GitlabSecurity/PublicSend
+
+ mount
+ end.keys
+ end
+
+ included do
+ after_save on: [:create, :update] do
+ background_upload(changed_mounts)
+ end
+ end
+ end
+
+ module Concern
+ extend ActiveSupport::Concern
+
+ included do |base|
+ base.include(ObjectStorage)
+
+ after :migrate, :delete_migrated_file
+ end
+
+ class_methods do
+ def object_store_options
+ options.object_store
+ end
+
+ def object_store_enabled?
+ object_store_options.enabled
+ end
+
+ def background_upload_enabled?
+ object_store_options.background_upload
+ end
+
+ def proxy_download_enabled?
+ object_store_options.proxy_download
+ end
+
+ def direct_download_enabled?
+ !proxy_download_enabled?
+ end
+
+ def object_store_credentials
+ object_store_options.connection.to_hash.deep_symbolize_keys
+ end
+
+ def remote_store_path
+ object_store_options.remote_directory
+ end
+
+ def serialization_column(model_class, mount_point)
+ model_class.uploader_options.dig(mount_point, :mount_on) || mount_point
+ end
+ end
+
+ def file_storage?
+ storage.is_a?(CarrierWave::Storage::File)
+ end
+
+ def file_cache_storage?
+ cache_storage.is_a?(CarrierWave::Storage::File)
+ end
+
+ def object_store
+ @object_store ||= model.try(store_serialization_column) || Store::LOCAL
+ end
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def object_store=(value)
+ @object_store = value || Store::LOCAL
+ @storage = storage_for(object_store)
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ # Return true if the current file is part or the model (i.e. is mounted in the model)
+ #
+ def persist_object_store?
+ model.respond_to?(:"#{store_serialization_column}=")
+ end
+
+ # Save the current @object_store to the model <mounted_as>_store column
+ def persist_object_store!
+ return unless persist_object_store?
+
+ updated = model.update_column(store_serialization_column, object_store)
+ raise 'Failed to update object store' unless updated
+ end
+
+ def use_file
+ if file_storage?
+ return yield path
+ end
+
+ begin
+ cache_stored_file!
+ yield cache_path
+ ensure
+ cache_storage.delete_dir!(cache_path(nil))
+ end
+ end
+
+ def filename
+ super || file&.filename
+ end
+
+ #
+ # Move the file to another store
+ #
+ # new_store: Enum (Store::LOCAL, Store::REMOTE)
+ #
+ def migrate!(new_store)
+ uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
+ raise 'Already running' unless uuid
+
+ unsafe_migrate!(new_store)
+ ensure
+ Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
+ end
+
+ def schedule_background_upload(*args)
+ return unless schedule_background_upload?
+
+ ObjectStorage::BackgroundMoveWorker.perform_async(self.class.name,
+ model.class.name,
+ mounted_as,
+ model.id)
+ end
+
+ def fog_directory
+ self.class.remote_store_path
+ end
+
+ def fog_credentials
+ self.class.object_store_credentials
+ end
+
+ def fog_public
+ false
+ end
+
+ def delete_migrated_file(migrated_file)
+ migrated_file.delete if exists?
+ end
+
+ def exists?
+ file.present?
+ end
+
+ def store_dir(store = nil)
+ store_dirs[store || object_store]
+ end
+
+ def store_dirs
+ {
+ Store::LOCAL => File.join(base_dir, dynamic_segment),
+ Store::REMOTE => File.join(dynamic_segment)
+ }
+ end
+
+ private
+
+ def schedule_background_upload?
+ self.class.object_store_enabled? &&
+ self.class.background_upload_enabled? &&
+ self.file_storage?
+ end
+
+ # this is a hack around CarrierWave. The #migrate method needs to be
+ # able to force the current file to the migrated file upon success.
+ def file=(file)
+ @file = file # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ def serialization_column
+ self.class.serialization_column(model.class, mounted_as)
+ end
+
+ # Returns the column where the 'store' is saved
+ # defaults to 'store'
+ def store_serialization_column
+ [serialization_column, 'store'].compact.join('_').to_sym
+ end
+
+ def storage
+ @storage ||= storage_for(object_store)
+ end
+
+ def storage_for(store)
+ case store
+ when Store::REMOTE
+ raise 'Object Storage is not enabled' unless self.class.object_store_enabled?
+
+ CarrierWave::Storage::Fog.new(self)
+ when Store::LOCAL
+ CarrierWave::Storage::File.new(self)
+ else
+ raise UnknownStoreError
+ end
+ end
+
+ def exclusive_lease_key
+ "object_storage_migrate:#{model.class}:#{model.id}"
+ end
+
+ #
+ # Move the file to another store
+ #
+ # new_store: Enum (Store::LOCAL, Store::REMOTE)
+ #
+ def unsafe_migrate!(new_store)
+ return unless object_store != new_store
+ return unless file
+
+ new_file = nil
+ file_to_delete = file
+ from_object_store = object_store
+ self.object_store = new_store # changes the storage and file
+
+ cache_stored_file! if file_storage?
+
+ with_callbacks(:migrate, file_to_delete) do
+ with_callbacks(:store, file_to_delete) do # for #store_versions!
+ new_file = storage.store!(file)
+ persist_object_store!
+ self.file = new_file
+ end
+ end
+
+ file
+ rescue => e
+ # in case of failure delete new file
+ new_file.delete unless new_file.nil?
+ # revert back to the old file
+ self.object_store = from_object_store
+ self.file = file_to_delete
+ raise e
+ end
+ end
+end
diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb
index f2ad0badd53..e3898b07730 100644
--- a/app/uploaders/personal_file_uploader.rb
+++ b/app/uploaders/personal_file_uploader.rb
@@ -4,7 +4,7 @@ class PersonalFileUploader < FileUploader
options.storage_path
end
- def self.base_dir(model)
+ def self.base_dir(model, _store = nil)
File.join(options.base_dir, model_path_segment(model))
end
@@ -14,6 +14,12 @@ class PersonalFileUploader < FileUploader
File.join(model.class.to_s.underscore, model.id.to_s)
end
+ def object_store
+ return Store::LOCAL unless model
+
+ super
+ end
+
# model_path_segment does not require a model to be passed, so we can always
# generate a path, even when there's no model.
def model_valid?
@@ -22,7 +28,14 @@ class PersonalFileUploader < FileUploader
# Revert-Override
def store_dir
- File.join(base_dir, dynamic_segment)
+ store_dirs[object_store]
+ end
+
+ def store_dirs
+ {
+ Store::LOCAL => File.join(base_dir, dynamic_segment),
+ Store::REMOTE => File.join(self.class.model_path_segment(model), dynamic_segment)
+ }
end
private
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
index 458928bc067..89c74a78835 100644
--- a/app/uploaders/records_uploads.rb
+++ b/app/uploaders/records_uploads.rb
@@ -24,8 +24,7 @@ module RecordsUploads
uploads.where(path: upload_path).delete_all
upload.destroy! if upload
- self.upload = build_upload
- upload.save!
+ self.upload = build_upload.tap(&:save!)
end
end
diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb
index 5239e70a326..b0c9a1b92a4 100644
--- a/app/validators/certificate_validator.rb
+++ b/app/validators/certificate_validator.rb
@@ -16,8 +16,6 @@ class CertificateValidator < ActiveModel::EachValidator
private
def valid_certificate_pem?(value)
- return false unless value
-
OpenSSL::X509::Certificate.new(value).present?
rescue OpenSSL::X509::CertificateError
false
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
index 37a314adee6..3ec1594e202 100644
--- a/app/validators/importable_url_validator.rb
+++ b/app/validators/importable_url_validator.rb
@@ -4,7 +4,7 @@
# protect against Server-side Request Forgery (SSRF).
class ImportableUrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- if Gitlab::UrlBlocker.blocked_url?(value)
+ if Gitlab::UrlBlocker.blocked_url?(value, valid_ports: Project::VALID_IMPORT_PORTS)
record.errors.add(attribute, "imports are not allowed from that URL")
end
end
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
new file mode 100644
index 00000000000..dd86c9ed2eb
--- /dev/null
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -0,0 +1,39 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :gravatar_enabled do
+ = f.check_box :gravatar_enabled
+ Gravatar enabled
+ .form-group
+ = f.label :default_projects_limit, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :default_projects_limit, class: 'form-control'
+ .form-group
+ = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_attachment_size, class: 'form-control'
+ .form-group
+ = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :session_expire_delay, class: 'form-control'
+ %span.help-block#session_expire_delay_help_block GitLab restart is required to apply changes
+ .form-group
+ = f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :user_oauth_applications do
+ = f.check_box :user_oauth_applications
+ Allow users to register any application to use GitLab as an OAuth provider
+ .form-group
+ = f.label :user_default_external, 'New users set to external', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :user_default_external do
+ = f.check_box :user_default_external
+ Newly registered users will by default be external
+
+ = f.submit 'Save changes', class: 'btn btn-success'
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 81d7db04a3c..0f75db3f6ae 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -2,254 +2,6 @@
= form_errors(@application_setting)
%fieldset
- %legend Visibility and Access Controls
- .form-group
- = f.label :default_branch_protection, class: 'control-label col-sm-2'
- .col-sm-10
- = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
- .form-group.visibility-level-setting
- = f.label :default_project_visibility, class: 'control-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
- .form-group.visibility-level-setting
- = f.label :default_snippet_visibility, class: 'control-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
- .form-group.visibility-level-setting
- = f.label :default_group_visibility, class: 'control-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
- .form-group
- = f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
- .col-sm-10
- - checkbox_name = 'application_setting[restricted_visibility_levels][]'
- = hidden_field_tag(checkbox_name)
- - restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level|
- .checkbox
- = level
- %span.help-block#restricted-visibility-help
- Selected levels cannot be used by non-admin users for projects or snippets.
- If the public level is restricted, user profiles are only visible to logged in users.
- .form-group
- = f.label :import_sources, class: 'control-label col-sm-2'
- .col-sm-10
- - import_sources_checkboxes('import-sources-help').each do |source|
- .checkbox= source
- %span.help-block#import-sources-help
- Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
- = link_to "(?)", help_page_path("integration/github")
- , Bitbucket
- = link_to "(?)", help_page_path("integration/bitbucket")
- and GitLab.com
- = link_to "(?)", help_page_path("integration/gitlab")
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :project_export_enabled do
- = f.check_box :project_export_enabled
- Project export enabled
-
- .form-group
- %label.control-label.col-sm-2 Enabled Git access protocols
- .col-sm-10
- = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
- %span.help-block#clone-protocol-help
- Allow only the selected protocols to be used for Git access.
-
- - ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
- - field_name = :"#{type}_key_restriction"
- .form-group
- = f.label field_name, "#{type.upcase} SSH keys", class: 'control-label col-sm-2'
- .col-sm-10
- = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
-
- %fieldset
- %legend Account and Limit Settings
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :gravatar_enabled do
- = f.check_box :gravatar_enabled
- Gravatar enabled
- .form-group
- = f.label :default_projects_limit, class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :default_projects_limit, class: 'form-control'
- .form-group
- = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :max_attachment_size, class: 'form-control'
- .form-group
- = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :session_expire_delay, class: 'form-control'
- %span.help-block#session_expire_delay_help_block GitLab restart is required to apply changes
- .form-group
- = f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :user_oauth_applications do
- = f.check_box :user_oauth_applications
- Allow users to register any application to use GitLab as an OAuth provider
- .form-group
- = f.label :user_default_external, 'New users set to external', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :user_default_external do
- = f.check_box :user_default_external
- Newly registered users will by default be external
-
- %fieldset
- %legend Sign-up Restrictions
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :signup_enabled do
- = f.check_box :signup_enabled
- Sign-up enabled
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :send_user_confirmation_email do
- = f.check_box :send_user_confirmation_email
- Send confirmation email on sign-up
- .form-group
- = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
- .help-block ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
- .form-group
- = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :domain_blacklist_enabled do
- = f.check_box :domain_blacklist_enabled
- Enable domain blacklist for sign ups
- .form-group
- .col-sm-offset-2.col-sm-10
- .radio
- = label_tag :blacklist_type_file do
- = radio_button_tag :blacklist_type, :file
- .option-title
- Upload blacklist file
- .radio
- = label_tag :blacklist_type_raw do
- = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?
- .option-title
- Enter blacklist manually
- .form-group.blacklist-file
- = f.label :domain_blacklist_file, 'Blacklist file', class: 'control-label col-sm-2'
- .col-sm-10
- = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
- .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
- .form-group.blacklist-raw
- = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
- .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
-
- .form-group
- = f.label :after_sign_up_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :after_sign_up_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
-
- %fieldset
- %legend Sign-in Restrictions
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :password_authentication_enabled_for_web do
- = f.check_box :password_authentication_enabled_for_web
- Password authentication enabled for web interface
- .help-block
- When disabled, an external authentication provider must be used.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :password_authentication_enabled_for_git do
- = f.check_box :password_authentication_enabled_for_git
- Password authentication enabled for Git over HTTP(S)
- .help-block
- When disabled, a Personal Access Token
- - if Gitlab::Auth::LDAP::Config.enabled?
- or LDAP password
- must be used to authenticate.
- - if omniauth_enabled? && button_based_providers.any?
- .form-group
- = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
- .col-sm-10
- .btn-group{ data: { toggle: 'buttons' } }
- - oauth_providers_checkboxes.each do |source|
- = source
- .form-group
- = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :require_two_factor_authentication do
- = f.check_box :require_two_factor_authentication
- Require all users to setup Two-factor authentication
- .form-group
- = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
- .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
- .form-group
- = f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
- %span.help-block#home_help_block We will redirect non-logged in users to this page
- .form-group
- = f.label :after_sign_out_path, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
- %span.help-block#after_sign_out_path_help_block We will redirect users to this page after they sign out
- .form-group
- = f.label :sign_in_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :sign_in_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
-
- %fieldset
- %legend Help Page
- .form-group
- = f.label :help_page_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :help_page_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :help_page_hide_commercial_content do
- = f.check_box :help_page_hide_commercial_content
- Hide marketing-related entries from help
- .form-group
- = f.label :help_page_support_url, 'Support page URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
- %span.help-block#support_help_block Alternate support URL for help page
-
- %fieldset
- %legend Pages
- .form-group
- = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :max_pages_size, class: 'form-control'
- .help-block 0 for unlimited
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :pages_domain_verification_enabled do
- = f.check_box :pages_domain_verification_enabled
- Require users to prove ownership of custom domains
- .help-block
- Domain verification is an essential security measure for public GitLab
- sites. Users are required to demonstrate they control a domain before
- it is enabled
- = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
-
- %fieldset
%legend Continuous Integration and Deployment
.form-group
.col-sm-offset-2.col-sm-10
@@ -860,5 +612,14 @@
.col-sm-10
= f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
+ %fieldset
+ %legend Outbound requests
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :allow_local_requests_from_hooks_and_services do
+ = f.check_box :allow_local_requests_from_hooks_and_services
+ Allow requests to the local network from hooks and services
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml
new file mode 100644
index 00000000000..3bc101ddf04
--- /dev/null
+++ b/app/views/admin/application_settings/_help_page.html.haml
@@ -0,0 +1,22 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :help_page_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :help_page_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :help_page_hide_commercial_content do
+ = f.check_box :help_page_hide_commercial_content
+ Hide marketing-related entries from help
+ .form-group
+ = f.label :help_page_support_url, 'Support page URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
+ %span.help-block#support_help_block Alternate support URL for help page
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
new file mode 100644
index 00000000000..b28ecf9a039
--- /dev/null
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -0,0 +1,22 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_pages_size, class: 'form-control'
+ .help-block 0 for unlimited
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :pages_domain_verification_enabled do
+ = f.check_box :pages_domain_verification_enabled
+ Require users to prove ownership of custom domains
+ .help-block
+ Domain verification is an essential security measure for public GitLab
+ sites. Users are required to demonstrate they control a domain before
+ it is enabled
+ = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
new file mode 100644
index 00000000000..864e64b5fa9
--- /dev/null
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -0,0 +1,59 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :password_authentication_enabled_for_web do
+ = f.check_box :password_authentication_enabled_for_web
+ Password authentication enabled for web interface
+ .help-block
+ When disabled, an external authentication provider must be used.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :password_authentication_enabled_for_git do
+ = f.check_box :password_authentication_enabled_for_git
+ Password authentication enabled for Git over HTTP(S)
+ .help-block
+ When disabled, a Personal Access Token
+ - if Gitlab::Auth::LDAP::Config.enabled?
+ or LDAP password
+ must be used to authenticate.
+ - if omniauth_enabled? && button_based_providers.any?
+ .form-group
+ = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
+ .col-sm-10
+ .btn-group{ data: { toggle: 'buttons' } }
+ - oauth_providers_checkboxes.each do |source|
+ = source
+ .form-group
+ = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :require_two_factor_authentication do
+ = f.check_box :require_two_factor_authentication
+ Require all users to setup Two-factor authentication
+ .form-group
+ = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
+ .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
+ .form-group
+ = f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
+ %span.help-block#home_help_block We will redirect non-logged in users to this page
+ .form-group
+ = f.label :after_sign_out_path, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
+ %span.help-block#after_sign_out_path_help_block We will redirect users to this page after they sign out
+ .form-group
+ = f.label :sign_in_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :sign_in_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml
new file mode 100644
index 00000000000..85f311dd894
--- /dev/null
+++ b/app/views/admin/application_settings/_signup.html.haml
@@ -0,0 +1,58 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :signup_enabled do
+ = f.check_box :signup_enabled
+ Sign-up enabled
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :send_user_confirmation_email do
+ = f.check_box :send_user_confirmation_email
+ Send confirmation email on sign-up
+ .form-group
+ = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
+ .help-block ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+ .form-group
+ = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :domain_blacklist_enabled do
+ = f.check_box :domain_blacklist_enabled
+ Enable domain blacklist for sign ups
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .radio
+ = label_tag :blacklist_type_file do
+ = radio_button_tag :blacklist_type, :file
+ .option-title
+ Upload blacklist file
+ .radio
+ = label_tag :blacklist_type_raw do
+ = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?
+ .option-title
+ Enter blacklist manually
+ .form-group.blacklist-file
+ = f.label :domain_blacklist_file, 'Blacklist file', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
+ .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
+ .form-group.blacklist-raw
+ = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
+ .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+
+ .form-group
+ = f.label :after_sign_up_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :after_sign_up_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
new file mode 100644
index 00000000000..cbc779548f6
--- /dev/null
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -0,0 +1,66 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :default_branch_protection, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
+ .form-group.visibility-level-setting
+ = f.label :default_project_visibility, class: 'control-label col-sm-2'
+ .col-sm-10
+ = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
+ .form-group.visibility-level-setting
+ = f.label :default_snippet_visibility, class: 'control-label col-sm-2'
+ .col-sm-10
+ = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
+ .form-group.visibility-level-setting
+ = f.label :default_group_visibility, class: 'control-label col-sm-2'
+ .col-sm-10
+ = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
+ .form-group
+ = f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
+ .col-sm-10
+ - checkbox_name = 'application_setting[restricted_visibility_levels][]'
+ = hidden_field_tag(checkbox_name)
+ - restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level|
+ .checkbox
+ = level
+ %span.help-block#restricted-visibility-help
+ Selected levels cannot be used by non-admin users for projects or snippets.
+ If the public level is restricted, user profiles are only visible to logged in users.
+ .form-group
+ = f.label :import_sources, class: 'control-label col-sm-2'
+ .col-sm-10
+ - import_sources_checkboxes('import-sources-help').each do |source|
+ .checkbox= source
+ %span.help-block#import-sources-help
+ Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
+ = link_to "(?)", help_page_path("integration/github")
+ , Bitbucket
+ = link_to "(?)", help_page_path("integration/bitbucket")
+ and GitLab.com
+ = link_to "(?)", help_page_path("integration/gitlab")
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :project_export_enabled do
+ = f.check_box :project_export_enabled
+ Project export enabled
+
+ .form-group
+ %label.control-label.col-sm-2 Enabled Git access protocols
+ .col-sm-10
+ = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
+ %span.help-block#clone-protocol-help
+ Allow only the selected protocols to be used for Git access.
+
+ - ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
+ - field_name = :"#{type}_key_restriction"
+ .form-group
+ = f.label field_name, "#{type.upcase} SSH keys", class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index ecc46d86afe..82d97f90248 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -1,5 +1,73 @@
+- breadcrumb_title "Settings"
- page_title "Settings"
+- @content_class = "limit-container-width" unless fluid_layout
+- expanded = Rails.env.test?
-%h3.page-title Settings
-%hr
-= render 'form'
+%section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Visibility and access controls')
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
+ .settings-content
+ = render 'visibility_and_access'
+
+%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Account and limit settings')
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Session expiration, projects limit and attachment size.')
+ .settings-content
+ = render 'account_and_limit'
+
+%section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Sign-up restrictions')
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure the way a user creates a new account.')
+ .settings-content
+ = render 'signup'
+
+%section.settings.as-signin.no-animate#js-signin-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Sign-in restrictions')
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
+ .settings-content
+ = render 'signin'
+
+%section.settings.as-help-page.no-animate#js-help-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Help page')
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Help page text and support page url.')
+ .settings-content
+ = render 'help_page'
+
+%section.settings.as-pages.no-animate#js-pages-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Pages')
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Size and domain settings for static websites')
+ .settings-content
+ = render 'pages'
+
+.prepend-top-20
+ = render 'form'
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 15201780451..5d4229c80af 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -10,7 +10,7 @@
- id_input_name = "#{form_field}[variables_attributes][][id]"
- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]"
- key_input_name = "#{form_field}[variables_attributes][][key]"
-- value_input_name = "#{form_field}[variables_attributes][][value]"
+- value_input_name = "#{form_field}[variables_attributes][][secret_value]"
- protected_input_name = "#{form_field}[variables_attributes][][protected]"
%li.js-row.ci-variable-row{ data: { is_persisted: "#{!id.nil?}" } }
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index df5841d1911..dec85368d10 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -13,13 +13,13 @@
.form-group
.input-group
- if current_user.can_select_namespace?
- .input-group-addon
+ .input-group-addon.has-tooltip{ title: root_url }
= root_url
= select_tag :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), class: 'select2 js-select-namespace', tabindex: 1
- else
- .input-group-addon.static-namespace
- #{root_url}#{current_user.username}/
+ .input-group-addon.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
+ #{user_url(current_user.username)}/
= hidden_field_tag :namespace_id, value: current_user.namespace_id
.form-group.col-xs-12.col-sm-6.project-path
= label_tag :path, 'Project name', class: 'label-light'
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index f0963cf9da8..f67a8878c80 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -6,6 +6,7 @@
.mobile-overlay
.alert-wrapper
= render "layouts/broadcast"
+ = render 'layouts/header/read_only_banner'
= yield :flash_message
- unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs"
diff --git a/app/views/layouts/header/_read_only_banner.html.haml b/app/views/layouts/header/_read_only_banner.html.haml
new file mode 100644
index 00000000000..f3d563c362f
--- /dev/null
+++ b/app/views/layouts/header/_read_only_banner.html.haml
@@ -0,0 +1,7 @@
+- message = read_only_message
+- if message
+ .flash-container.flash-container-page
+ .flash-notice
+ %div{ class: (container_class) }
+ %span
+ = message
diff --git a/app/views/notify/push_to_merge_request_email.html.haml b/app/views/notify/push_to_merge_request_email.html.haml
new file mode 100644
index 00000000000..5cc6f21c0f3
--- /dev/null
+++ b/app/views/notify/push_to_merge_request_email.html.haml
@@ -0,0 +1,26 @@
+%h3
+ New commits were pushed to the merge request
+ = link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request))
+ by #{@current_user.name}
+
+- if @existing_commits.any?
+ - count = @existing_commits.size
+ %ul
+ %li
+ - if count.one?
+ - commit_id = @existing_commits.first[:short_id]
+ = link_to(commit_id, project_commit_url(@merge_request.target_project, commit_id))
+ - else
+ = link_to(project_compare_url(@merge_request.target_project, from: @existing_commits.first[:short_id], to: @existing_commits.last[:short_id])) do
+ #{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]}
+ = precede '&nbsp;- ' do
+ - commits_text = "#{count} commit".pluralize(count)
+ #{commits_text} from branch `#{@merge_request.target_branch}`
+
+- if @new_commits.any?
+ %ul
+ - @new_commits.each do |commit|
+ %li
+ = link_to(commit[:short_id], project_commit_url(@merge_request.target_project, commit[:short_id]))
+ = precede ' - ' do
+ #{commit[:title]}
diff --git a/app/views/notify/push_to_merge_request_email.text.haml b/app/views/notify/push_to_merge_request_email.text.haml
new file mode 100644
index 00000000000..d7722e5f41f
--- /dev/null
+++ b/app/views/notify/push_to_merge_request_email.text.haml
@@ -0,0 +1,13 @@
+New commits were pushed to the merge request #{@merge_request.to_reference} by #{@current_user.name}
+\
+#{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))}
+\
+- if @existing_commits.any?
+ - count = @existing_commits.size
+ - commits_id = count.one? ? @existing_commits.first[:short_id] : "#{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]}"
+ - commits_text = "#{count} commit".pluralize(count)
+
+ * #{commits_id} - #{commits_text} from branch `#{@merge_request.target_branch}`
+\
+- @new_commits.each do |commit|
+ * #{commit[:short_id]} - #{raw commit[:title]}
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 6f5eb828902..6a1035d2dc7 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -13,6 +13,6 @@
#{time_ago_with_tooltip(event.created_at)}
- .pull-right
+ .flex-right
= link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do
#{ _('Create merge request') }
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index f4b5ef1555e..241bc3dbca0 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -9,12 +9,12 @@
Project path
.input-group
- if current_user.can_select_namespace?
- .input-group-addon
+ .input-group-addon.has-tooltip{ title: root_url }
= root_url
= f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1}
- else
- .input-group-addon.static-namespace
+ .input-group-addon.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
#{user_url(current_user.username)}/
= f.hidden_field :namespace_id, value: current_user.namespace_id
.form-group.project-path.col-sm-6
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 1da0e865a41..883dfb3e6c8 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -5,81 +5,82 @@
- number_commits_behind = diverging_commit_counts[:behind]
- number_commits_ahead = diverging_commit_counts[:ahead]
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
-%li{ class: "js-branch-#{branch.name}" }
- %div
- = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated ref-name' do
- = sprite_icon('fork', size: 12)
- = branch.name
- &nbsp;
- - if branch.name == @repository.root_ref
- %span.label.label-primary default
- - elsif merged
- %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
- = s_('Branches|merged')
+%li{ class: "branch-item js-branch-#{branch.name}" }
+ .branch-info
+ .branch-title
+ = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name' do
+ = sprite_icon('fork', size: 12)
+ = branch.name
+ &nbsp;
+ - if branch.name == @repository.root_ref
+ %span.label.label-primary default
+ - elsif merged
+ %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
+ = s_('Branches|merged')
- - if protected_branch?(@project, branch)
- %span.label.label-success
- = s_('Branches|protected')
- .controls.hidden-xs<
- - if merge_project && create_mr_button?(@repository.root_ref, branch.name)
- = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
- = _('Merge request')
+ - if protected_branch?(@project, branch)
+ %span.label.label-success
+ = s_('Branches|protected')
- - if branch.name != @repository.root_ref
- = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name),
- class: "btn btn-default #{'prepend-left-10' unless merge_project}",
- method: :post,
- title: s_('Branches|Compare') do
- = s_('Branches|Compare')
+ .block-truncated
+ - if commit
+ = render 'projects/branches/commit', commit: commit, project: @project
+ - else
+ = s_('Branches|Cant find HEAD commit for this branch')
- = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name]
+ - if branch.name != @repository.root_ref
+ .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
+ default_branch: @repository.root_ref,
+ number_commits_ahead: diverging_count_label(number_commits_ahead) } }
+ .graph-side
+ .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
+ %span.count.count-behind= diverging_count_label(number_commits_behind)
+ .graph-separator
+ .graph-side
+ .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
+ %span.count.count-ahead= diverging_count_label(number_commits_ahead)
- - if can?(current_user, :push_code, @project)
- - if branch.name == @project.repository.root_ref
- %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
- disabled: true,
- title: s_('Branches|The default branch cannot be deleted') }
- = icon("trash-o")
- - elsif protected_branch?(@project, branch)
- - if can?(current_user, :delete_protected_branch, @project)
- %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
- title: s_('Branches|Delete protected branch'),
- data: { toggle: "modal",
- target: "#modal-delete-branch",
- delete_path: project_branch_path(@project, branch.name),
- branch_name: branch.name,
- is_merged: ("true" if merged) } }
- = icon("trash-o")
- - else
- %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
- disabled: true,
- title: s_('Branches|Only a project master or owner can delete a protected branch') }
- = icon("trash-o")
- - else
- = link_to project_branch_path(@project, branch.name),
- class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
- title: s_('Branches|Delete branch'),
- method: :delete,
- data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } },
- remote: true,
- 'aria-label' => s_('Branches|Delete branch') do
- = icon("trash-o")
+ .controls.hidden-xs<
+ - if merge_project && create_mr_button?(@repository.root_ref, branch.name)
+ = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
+ = _('Merge request')
- if branch.name != @repository.root_ref
- .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
- default_branch: @repository.root_ref,
- number_commits_ahead: diverging_count_label(number_commits_ahead) } }
- .graph-side
- .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
- %span.count.count-behind= diverging_count_label(number_commits_behind)
- .graph-separator
- .graph-side
- .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
- %span.count.count-ahead= diverging_count_label(number_commits_ahead)
+ = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name),
+ class: "btn btn-default #{'prepend-left-10' unless merge_project}",
+ method: :post,
+ title: s_('Branches|Compare') do
+ = s_('Branches|Compare')
+ = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name]
- - if commit
- = render 'projects/branches/commit', commit: commit, project: @project
- - else
- %p
- = s_('Branches|Cant find HEAD commit for this branch')
+ - if can?(current_user, :push_code, @project)
+ - if branch.name == @project.repository.root_ref
+ %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
+ disabled: true,
+ title: s_('Branches|The default branch cannot be deleted') }
+ = icon("trash-o")
+ - elsif protected_branch?(@project, branch)
+ - if can?(current_user, :delete_protected_branch, @project)
+ %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
+ title: s_('Branches|Delete protected branch'),
+ data: { toggle: "modal",
+ target: "#modal-delete-branch",
+ delete_path: project_branch_path(@project, branch.name),
+ branch_name: branch.name,
+ is_merged: ("true" if merged) } }
+ = icon("trash-o")
+ - else
+ %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
+ disabled: true,
+ title: s_('Branches|Only a project master or owner can delete a protected branch') }
+ = icon("trash-o")
+ - else
+ = link_to project_branch_path(@project, branch.name),
+ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
+ title: s_('Branches|Delete branch'),
+ method: :delete,
+ data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } },
+ remote: true,
+ 'aria-label' => s_('Branches|Delete branch') do
+ = icon("trash-o")
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index e779473c239..ecf186e3dc8 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -35,7 +35,7 @@
= link_to download_project_job_artifacts_path(@project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
Download
- - if @build.artifacts_metadata?
+ - if @build.browsable_artifacts?
= link_to browse_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default' do
Browse
diff --git a/app/views/projects/pages/_https_only.html.haml b/app/views/projects/pages/_https_only.html.haml
new file mode 100644
index 00000000000..6a3ffce949f
--- /dev/null
+++ b/app/views/projects/pages/_https_only.html.haml
@@ -0,0 +1,10 @@
+= form_for @project, url: namespace_project_pages_path(@project.namespace.becomes(Namespace), @project), html: { class: 'inline', title: pages_https_only_title } do |f|
+ = f.check_box :pages_https_only, class: 'pull-left', disabled: pages_https_only_disabled?
+
+ .prepend-left-20
+ = f.label :pages_https_only, class: pages_https_only_label_class do
+ %strong Force domains with SSL certificates to use HTTPS
+
+ - unless pages_https_only_disabled?
+ .prepend-top-10
+ = f.submit 'Save', class: 'btn btn-success'
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index 04e647c0dc6..f17d9d24db6 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -13,6 +13,9 @@
Combined with the power of GitLab CI and the help of GitLab Runner
you can deploy static pages for your individual projects, your user or your group.
+- if Gitlab.config.pages.external_https
+ = render 'https_only'
+
%hr.clearfix
= render 'access'
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index f65e8385ac8..9a11cdb121e 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -39,6 +39,10 @@
- github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository
+- object_storage_upload
+- object_storage:object_storage_background_move
+- object_storage:object_storage_migrate_uploads
+
- pipeline_cache:expire_job_cache
- pipeline_cache:expire_pipeline_cache
- pipeline_creation:create_pipeline
diff --git a/app/workers/concerns/object_storage_queue.rb b/app/workers/concerns/object_storage_queue.rb
new file mode 100644
index 00000000000..a80f473a6d4
--- /dev/null
+++ b/app/workers/concerns/object_storage_queue.rb
@@ -0,0 +1,8 @@
+# Concern for setting Sidekiq settings for the various GitLab ObjectStorage workers.
+module ObjectStorageQueue
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :object_storage
+ end
+end
diff --git a/app/workers/object_storage/background_move_worker.rb b/app/workers/object_storage/background_move_worker.rb
new file mode 100644
index 00000000000..9c4d72e0ecf
--- /dev/null
+++ b/app/workers/object_storage/background_move_worker.rb
@@ -0,0 +1,29 @@
+module ObjectStorage
+ class BackgroundMoveWorker
+ include ApplicationWorker
+ include ObjectStorageQueue
+
+ sidekiq_options retry: 5
+
+ def perform(uploader_class_name, subject_class_name, file_field, subject_id)
+ uploader_class = uploader_class_name.constantize
+ subject_class = subject_class_name.constantize
+
+ return unless uploader_class < ObjectStorage::Concern
+ return unless uploader_class.object_store_enabled?
+ return unless uploader_class.background_upload_enabled?
+
+ subject = subject_class.find(subject_id)
+ uploader = build_uploader(subject, file_field&.to_sym)
+ uploader.migrate!(ObjectStorage::Store::REMOTE)
+ end
+
+ def build_uploader(subject, mount_point)
+ case subject
+ when Upload then subject.build_uploader(mount_point)
+ else
+ subject.send(mount_point) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+end
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
new file mode 100644
index 00000000000..01ed123e6c8
--- /dev/null
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -0,0 +1,202 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/LineLength
+# rubocop:disable Style/Documentation
+
+module ObjectStorage
+ class MigrateUploadsWorker
+ include ApplicationWorker
+ include ObjectStorageQueue
+
+ SanityCheckError = Class.new(StandardError)
+
+ class Upload < ActiveRecord::Base
+ # Upper limit for foreground checksum processing
+ CHECKSUM_THRESHOLD = 100.megabytes
+
+ belongs_to :model, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+
+ validates :size, presence: true
+ validates :path, presence: true
+ validates :model, presence: true
+ validates :uploader, presence: true
+
+ before_save :calculate_checksum!, if: :foreground_checksummable?
+ after_commit :schedule_checksum, if: :checksummable?
+
+ scope :stored_locally, -> { where(store: [nil, ObjectStorage::Store::LOCAL]) }
+ scope :stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) }
+
+ def self.hexdigest(path)
+ Digest::SHA256.file(path).hexdigest
+ end
+
+ def absolute_path
+ raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local?
+ return path unless relative_path?
+
+ uploader_class.absolute_path(self)
+ end
+
+ def calculate_checksum!
+ self.checksum = nil
+ return unless checksummable?
+
+ self.checksum = self.class.hexdigest(absolute_path)
+ end
+
+ def build_uploader(mounted_as = nil)
+ uploader_class.new(model, mounted_as).tap do |uploader|
+ uploader.upload = self
+ uploader.retrieve_from_store!(identifier)
+ end
+ end
+
+ def exist?
+ File.exist?(absolute_path)
+ end
+
+ def local?
+ return true if store.nil?
+
+ store == ObjectStorage::Store::LOCAL
+ end
+
+ private
+
+ def checksummable?
+ checksum.nil? && local? && exist?
+ end
+
+ def foreground_checksummable?
+ checksummable? && size <= CHECKSUM_THRESHOLD
+ end
+
+ def schedule_checksum
+ UploadChecksumWorker.perform_async(id)
+ end
+
+ def relative_path?
+ !path.start_with?('/')
+ end
+
+ def identifier
+ File.basename(path)
+ end
+
+ def uploader_class
+ Object.const_get(uploader)
+ end
+ end
+
+ class MigrationResult
+ attr_reader :upload
+ attr_accessor :error
+
+ def initialize(upload, error = nil)
+ @upload, @error = upload, error
+ end
+
+ def success?
+ error.nil?
+ end
+
+ def to_s
+ success? ? "Migration successful." : "Error while migrating #{upload.id}: #{error.message}"
+ end
+ end
+
+ module Report
+ class MigrationFailures < StandardError
+ attr_reader :errors
+
+ def initialize(errors)
+ @errors = errors
+ end
+
+ def message
+ errors.map(&:message).join("\n")
+ end
+ end
+
+ def report!(results)
+ success, failures = results.partition(&:success?)
+
+ Rails.logger.info header(success, failures)
+ Rails.logger.warn failures(failures)
+
+ raise MigrationFailures.new(failures.map(&:error)) if failures.any?
+ end
+
+ def header(success, failures)
+ "Migrated #{success.count}/#{success.count + failures.count} files."
+ end
+
+ def failures(failures)
+ failures.map { |f| "\t#{f}" }.join('\n')
+ end
+ end
+
+ include Report
+
+ def self.enqueue!(uploads, mounted_as, to_store)
+ sanity_check!(uploads, mounted_as)
+
+ perform_async(uploads.ids, mounted_as, to_store)
+ end
+
+ # We need to be sure all the uploads are for the same uploader and model type
+ # and that the mount point exists if provided.
+ #
+ def self.sanity_check!(uploads, mounted_as)
+ upload = uploads.first
+
+ uploader_class = upload.uploader.constantize
+ model_class = uploads.first.model_type.constantize
+
+ uploader_types = uploads.map(&:uploader).uniq
+ model_types = uploads.map(&:model_type).uniq
+ model_has_mount = mounted_as.nil? || model_class.uploaders[mounted_as] == uploader_class
+
+ raise(SanityCheckError, "Multiple uploaders found: #{uploader_types}") unless uploader_types.count == 1
+ raise(SanityCheckError, "Multiple model types found: #{model_types}") unless model_types.count == 1
+ raise(SanityCheckError, "Mount point #{mounted_as} not found in #{model_class}.") unless model_has_mount
+ end
+
+ def perform(ids, mounted_as, to_store)
+ @mounted_as = mounted_as&.to_sym
+ @to_store = to_store
+
+ uploads = Upload.preload(:model).where(id: ids)
+
+ sanity_check!(uploads)
+ results = migrate(uploads)
+
+ report!(results)
+ rescue SanityCheckError => e
+ # do not retry: the job is insane
+ Rails.logger.warn "#{self.class}: Sanity check error (#{e.message})"
+ end
+
+ def sanity_check!(uploads)
+ self.class.sanity_check!(uploads, @mounted_as)
+ end
+
+ def build_uploaders(uploads)
+ uploads.map { |upload| upload.build_uploader(@mounted_as) }
+ end
+
+ def migrate(uploads)
+ build_uploaders(uploads).map(&method(:process_uploader))
+ end
+
+ def process_uploader(uploader)
+ MigrationResult.new(uploader.upload).tap do |result|
+ begin
+ uploader.migrate!(@to_store)
+ rescue => e
+ result.error = e
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/object_storage_upload_worker.rb b/app/workers/object_storage_upload_worker.rb
new file mode 100644
index 00000000000..5c80f34069c
--- /dev/null
+++ b/app/workers/object_storage_upload_worker.rb
@@ -0,0 +1,21 @@
+# @Deprecated - remove once the `object_storage_upload` queue is empty
+# The queue has been renamed `object_storage:object_storage_background_upload`
+#
+class ObjectStorageUploadWorker
+ include ApplicationWorker
+
+ sidekiq_options retry: 5
+
+ def perform(uploader_class_name, subject_class_name, file_field, subject_id)
+ uploader_class = uploader_class_name.constantize
+ subject_class = subject_class_name.constantize
+
+ return unless uploader_class < ObjectStorage::Concern
+ return unless uploader_class.object_store_enabled?
+ return unless uploader_class.background_upload_enabled?
+
+ subject = subject_class.find(subject_id)
+ uploader = subject.public_send(file_field) # rubocop:disable GitlabSecurity/PublicSend
+ uploader.migrate!(ObjectStorage::Store::REMOTE)
+ end
+end