summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2018-04-03 11:10:01 +0100
committerPhil Hughes <me@iamphill.com>2018-04-03 11:10:01 +0100
commit49f198e6c142c0bf6983187b164599388cd67197 (patch)
treef103b1914c092480af36c200ca75813d6b132434 /app
parentd6dd2f5f70685e35cf6d0f2738059fe44e1054a3 (diff)
parent8dca091ff7f04bb92a7835ebeff783b7f0ef76cd (diff)
downloadgitlab-ce-49f198e6c142c0bf6983187b164599388cd67197.tar.gz
Merge branch 'master' into ide-staged-changes
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js146
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue87
-rw-r--r--app/assets/javascripts/ide/components/ide.vue85
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue23
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue29
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue24
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue40
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue71
-rw-r--r--app/assets/javascripts/ide/ide_router.js60
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js28
-rw-r--r--app/assets/javascripts/ide/lib/editor.js20
-rw-r--r--app/assets/javascripts/ide/services/index.js25
-rw-r--r--app/assets/javascripts/ide/stores/actions.js11
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js73
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js84
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js98
-rw-r--r--app/assets/javascripts/ide/stores/getters.js18
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js8
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js12
-rw-r--r--app/assets/javascripts/ide/stores/mutations/merge_request.js33
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js1
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js10
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue101
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue53
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue21
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue66
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue23
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue114
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue97
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/repo.scss25
-rw-r--r--app/assets/stylesheets/pages/repo.scss.orig786
-rw-r--r--app/controllers/admin/appearances_controller.rb18
-rw-r--r--app/controllers/projects/services_controller.rb2
-rw-r--r--app/helpers/appearances_helper.rb16
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/emails_helper.rb4
-rw-r--r--app/models/ci/artifact_blob.rb7
-rw-r--r--app/models/ci/build.rb127
-rw-r--r--app/models/clusters/cluster.rb4
-rw-r--r--app/models/clusters/concerns/application_status.rb2
-rw-r--r--app/models/issue.rb6
-rw-r--r--app/models/project.rb35
-rw-r--r--app/services/boards/list_service.rb6
-rw-r--r--app/services/issues/close_service.rb1
-rw-r--r--app/services/projects/import_export/export_service.rb33
-rw-r--r--app/services/projects/import_service.rb6
-rw-r--r--app/services/projects/update_pages_service.rb3
-rw-r--r--app/validators/certificate_fingerprint_validator.rb9
-rw-r--r--app/validators/importable_url_validator.rb6
-rw-r--r--app/validators/top_level_group_validator.rb7
-rw-r--r--app/views/admin/application_settings/_background_jobs.html.haml30
-rw-r--r--app/views/admin/application_settings/_form.html.haml105
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml16
-rw-r--r--app/views/admin/application_settings/_spam.html.haml65
-rw-r--r--app/views/admin/application_settings/show.html.haml52
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/devise_empty.html.haml2
-rw-r--r--app/views/projects/_export.html.haml2
-rw-r--r--app/views/projects/clusters/show.html.haml4
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/edit.html.haml8
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/no_repo.html.haml6
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml8
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml5
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb30
-rw-r--r--app/workers/project_export_worker.rb14
76 files changed, 2168 insertions, 782 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index cbcefb2c18f..8ad3d18b302 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -10,6 +10,9 @@ const Api = {
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
+ mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
+ mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
+ mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
@@ -22,25 +25,27 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
- const url = Api.buildUrl(Api.groupPath)
- .replace(':id', groupId);
- return axios.get(url)
- .then(({ data }) => {
- callback(data);
+ const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
+ return axios.get(url).then(({ data }) => {
+ callback(data);
- return data;
- });
+ return data;
+ });
},
// Return groups list. Filtered by query
groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath);
- return axios.get(url, {
- params: Object.assign({
- search: query,
- per_page: 20,
- }, options),
- })
+ return axios
+ .get(url, {
+ params: Object.assign(
+ {
+ search: query,
+ per_page: 20,
+ },
+ options,
+ ),
+ })
.then(({ data }) => {
callback(data);
@@ -51,12 +56,13 @@ const Api = {
// Return namespaces list. Filtered by query
namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath);
- return axios.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
- })
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ },
+ })
.then(({ data }) => callback(data));
},
@@ -73,9 +79,10 @@ const Api = {
defaults.membership = true;
}
- return axios.get(url, {
- params: Object.assign(defaults, options),
- })
+ return axios
+ .get(url, {
+ params: Object.assign(defaults, options),
+ })
.then(({ data }) => {
callback(data);
@@ -85,8 +92,32 @@ const Api = {
// Return single project
project(projectPath) {
- const url = Api.buildUrl(Api.projectPath)
- .replace(':id', encodeURIComponent(projectPath));
+ const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
+
+ return axios.get(url);
+ },
+
+ // Return Merge Request for project
+ mergeRequest(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
+
+ return axios.get(url);
+ },
+
+ mergeRequestChanges(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestChangesPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
+
+ return axios.get(url);
+ },
+
+ mergeRequestVersions(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestVersionsPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
return axios.get(url);
},
@@ -102,30 +133,30 @@ const Api = {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
}
- return axios.post(url, {
- label: data,
- })
+ return axios
+ .post(url, {
+ label: data,
+ })
.then(res => callback(res.data))
.catch(e => callback(e.response.data));
},
// Return group projects list. Filtered by query
groupProjects(groupId, query, callback) {
- const url = Api.buildUrl(Api.groupProjectsPath)
- .replace(':id', groupId);
- return axios.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
- })
+ const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ },
+ })
.then(({ data }) => callback(data));
},
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
- const url = Api.buildUrl(Api.commitPath)
- .replace(':id', encodeURIComponent(id));
+ const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
return axios.post(url, JSON.stringify(data), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
@@ -136,39 +167,34 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id))
- .replace(':branch', branch);
+ .replace(':branch', encodeURIComponent(branch));
return axios.get(url);
},
// Return text for a specific license
licenseText(key, data, callback) {
- const url = Api.buildUrl(Api.licensePath)
- .replace(':key', key);
- return axios.get(url, {
- params: data,
- })
+ const url = Api.buildUrl(Api.licensePath).replace(':key', key);
+ return axios
+ .get(url, {
+ params: data,
+ })
.then(res => callback(res.data));
},
gitignoreText(key, callback) {
- const url = Api.buildUrl(Api.gitignorePath)
- .replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
+ return axios.get(url).then(({ data }) => callback(data));
},
gitlabCiYml(key, callback) {
- const url = Api.buildUrl(Api.gitlabCiYmlPath)
- .replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
+ return axios.get(url).then(({ data }) => callback(data));
},
dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ return axios.get(url).then(({ data }) => callback(data));
},
issueTemplate(namespacePath, projectPath, key, type, callback) {
@@ -177,7 +203,8 @@ const Api = {
.replace(':type', type)
.replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath);
- return axios.get(url)
+ return axios
+ .get(url)
.then(({ data }) => callback(null, data))
.catch(callback);
},
@@ -185,10 +212,13 @@ const Api = {
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
- params: Object.assign({
- search: query,
- per_page: 20,
- }, options),
+ params: Object.assign(
+ {
+ search: query,
+ per_page: 20,
+ },
+ options,
+ ),
});
},
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 170347881e0..0c44a755f56 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,31 +1,44 @@
<script>
- import Icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __, sprintf } from '~/locale';
- export default {
- components: {
- Icon,
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ hasChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- props: {
- hasChanges: {
- type: Boolean,
- required: false,
- default: false,
- },
- viewer: {
- type: String,
- required: true,
- },
- showShadow: {
- type: Boolean,
- required: true,
- },
+ mergeRequestId: {
+ type: String,
+ required: false,
+ default: '',
},
- methods: {
- changeMode(mode) {
- this.$emit('click', mode);
- },
+ viewer: {
+ type: String,
+ required: true,
},
- };
+ showShadow: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ mergeReviewLine() {
+ return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
+ mergeRequestId: this.mergeRequestId,
+ });
+ },
+ },
+ methods: {
+ changeMode(mode) {
+ this.$emit('click', mode);
+ },
+ },
+};
</script>
<template>
@@ -43,7 +56,10 @@
}"
data-toggle="dropdown"
>
- <template v-if="viewer === 'editor'">
+ <template v-if="viewer === 'mrdiff' && mergeRequestId">
+ {{ mergeReviewLine }}
+ </template>
+ <template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
@@ -57,6 +73,29 @@
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
+ <template v-if="mergeRequestId">
+ <li>
+ <a
+ href="#"
+ @click.prevent="changeMode('mrdiff')"
+ :class="{
+ 'is-active': viewer === 'mrdiff',
+ }"
+ >
+ <strong class="dropdown-menu-inner-title">
+ {{ mergeReviewLine }}
+ </strong>
+ <span class="dropdown-menu-inner-content">
+ {{ __('Compare changes with the merge request target branch') }}
+ </span>
+ </a>
+ </li>
+ <li
+ role="separator"
+ class="divider"
+ >
+ </li>
+ </template>
<li>
<a
href="#"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 015e750525a..5e44af01241 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,51 +1,51 @@
<script>
- import { mapState, mapGetters } from 'vuex';
- import ideSidebar from './ide_side_bar.vue';
- import ideContextbar from './ide_context_bar.vue';
- import repoTabs from './repo_tabs.vue';
- import repoFileButtons from './repo_file_buttons.vue';
- import ideStatusBar from './ide_status_bar.vue';
- import repoEditor from './repo_editor.vue';
+import { mapState, mapGetters } from 'vuex';
+import ideSidebar from './ide_side_bar.vue';
+import ideContextbar from './ide_context_bar.vue';
+import repoTabs from './repo_tabs.vue';
+import repoFileButtons from './repo_file_buttons.vue';
+import ideStatusBar from './ide_status_bar.vue';
+import repoEditor from './repo_editor.vue';
- export default {
- components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- repoFileButtons,
- ideStatusBar,
- repoEditor,
+export default {
+ components: {
+ ideSidebar,
+ ideContextbar,
+ repoTabs,
+ repoFileButtons,
+ ideStatusBar,
+ repoEditor,
+ },
+ props: {
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
},
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
},
- computed: {
- ...mapState(['changedFiles', 'openFiles', 'viewer']),
- ...mapGetters(['activeFile', 'hasChanges']),
+ committedStateSvgPath: {
+ type: String,
+ required: true,
},
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = e => {
- if (!this.changedFiles.length) return undefined;
+ },
+ computed: {
+ ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
+ ...mapGetters(['activeFile', 'hasChanges']),
+ },
+ mounted() {
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = e => {
+ if (!this.changedFiles.length) return undefined;
- Object.assign(e, {
- returnValue,
- });
- return returnValue;
- };
- },
- };
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
+ },
+};
</script>
<template>
@@ -63,6 +63,7 @@
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
+ :merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
new file mode 100644
index 00000000000..8a440902dfc
--- /dev/null
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -0,0 +1,23 @@
+<script>
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+};
+</script>
+
+<template>
+ <icon
+ name="git-merge"
+ v-tooltip
+ title="__('Part of merge request changes')"
+ css-classes="ide-file-changed-icon"
+ :size="12"
+ />
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index e73d1ce839f..b6f8f8a1c99 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,6 +1,6 @@
<script>
/* global monaco */
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
@@ -13,12 +13,8 @@ export default {
},
},
computed: {
- ...mapState([
- 'leftPanelCollapsed',
- 'rightPanelCollapsed',
- 'viewer',
- 'delayViewerUpdated',
- ]),
+ ...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
+ ...mapGetters(['currentMergeRequest']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
},
@@ -68,9 +64,14 @@ export default {
this.editor.clearEditor();
- this.getRawFileData(this.file)
+ this.getRawFileData({
+ path: this.file.path,
+ baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
+ })
.then(() => {
- const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
+ const viewerPromise = this.delayViewerUpdated
+ ? this.updateViewer('editor')
+ : Promise.resolve();
return viewerPromise;
})
@@ -78,7 +79,7 @@ export default {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
- .catch((err) => {
+ .catch(err => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
@@ -101,9 +102,13 @@ export default {
this.model = this.editor.createModel(this.file);
- this.editor.attachModel(this.model);
+ if (this.viewer === 'mrdiff') {
+ this.editor.attachMergeRequestModel(this.model);
+ } else {
+ this.editor.attachModel(this.model);
+ }
- this.model.onChange((model) => {
+ this.model.onChange(model => {
const { file } = model;
if (file.active) {
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 9a7c83ea7b9..fab53592e4e 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -6,6 +6,7 @@ import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
+import mrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
@@ -15,6 +16,7 @@ export default {
fileStatusIcon,
fileIcon,
changedFileIcon,
+ mrFileIcon,
},
props: {
file: {
@@ -56,10 +58,7 @@ export default {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() {
// Manual Action if a tree is selected/opened
- if (
- this.isTree &&
- this.$router.currentRoute.path === `/project${this.file.url}`
- ) {
+ if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
@@ -102,12 +101,17 @@ export default {
:file="file"
/>
</span>
- <changed-file-icon
- v-if="file.changed || file.tempFile || file.staged"
- :file="file"
- :show-tooltip="true"
- class="prepend-top-5 pull-right"
- />
+ <span class="pull-right">
+ <mr-file-icon
+ v-if="file.mrChange"
+ />
+ <changed-file-icon
+ v-if="file.changed || file.tempFile || file.staged"
+ :file="file"
+ :show-tooltip="true"
+ class="prepend-top-5 pull-right"
+ />
+ </span>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 25d311142d5..97589e116c5 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -1,27 +1,27 @@
<script>
- import icon from '~/vue_shared/components/icon.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
- import '~/lib/utils/datetime_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import '~/lib/utils/datetime_utility';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ },
+ computed: {
+ lockTooltip() {
+ return `Locked by ${this.file.file_lock.user.name}`;
},
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- lockTooltip() {
- return `Locked by ${this.file.file_lock.user.name}`;
- },
- },
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 8ea64ddf84a..a44e418b2eb 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -1,42 +1,46 @@
<script>
- import { mapActions } from 'vuex';
- import RepoTab from './repo_tab.vue';
- import EditorMode from './editor_mode_dropdown.vue';
+import { mapActions } from 'vuex';
+import RepoTab from './repo_tab.vue';
+import EditorMode from './editor_mode_dropdown.vue';
- export default {
- components: {
- RepoTab,
- EditorMode,
+export default {
+ components: {
+ RepoTab,
+ EditorMode,
+ },
+ props: {
+ files: {
+ type: Array,
+ required: true,
},
- props: {
- files: {
- type: Array,
- required: true,
- },
- viewer: {
- type: String,
- required: true,
- },
- hasChanges: {
- type: Boolean,
- required: true,
- },
+ viewer: {
+ type: String,
+ required: true,
},
- data() {
- return {
- showShadow: false,
- };
+ hasChanges: {
+ type: Boolean,
+ required: true,
},
- updated() {
- if (!this.$refs.tabsScroller) return;
-
- this.showShadow =
- this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
- },
- methods: {
- ...mapActions(['updateViewer']),
+ mergeRequestId: {
+ type: String,
+ required: false,
+ default: '',
},
- };
+ },
+ data() {
+ return {
+ showShadow: false,
+ };
+ },
+ updated() {
+ if (!this.$refs.tabsScroller) return;
+
+ this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
</script>
<template>
@@ -55,6 +59,7 @@
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
+ :merge-request-id="mergeRequestId"
@click="updateViewer"
/>
</div>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index db89c1d44db..be2c12c0487 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -44,7 +44,7 @@ const router = new VueRouter({
component: EmptyRouterComponent,
},
{
- path: 'mr/:mrid',
+ path: 'merge_requests/:mrid',
component: EmptyRouterComponent,
},
],
@@ -76,9 +76,7 @@ router.beforeEach((to, from, next) => {
.then(() => {
if (to.params[0]) {
const path =
- to.params[0].slice(-1) === '/'
- ? to.params[0].slice(0, -1)
- : to.params[0];
+ to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
const treeEntry = store.state.entries[path];
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
@@ -96,6 +94,60 @@ router.beforeEach((to, from, next) => {
);
throw e;
});
+ } else if (to.params.mrid) {
+ store.dispatch('updateViewer', 'mrdiff');
+
+ store
+ .dispatch('getMergeRequestData', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ })
+ .then(mr => {
+ store.dispatch('getBranchData', {
+ projectId: fullProjectId,
+ branchId: mr.source_branch,
+ });
+
+ return store.dispatch('getFiles', {
+ projectId: fullProjectId,
+ branchId: mr.source_branch,
+ });
+ })
+ .then(() =>
+ store.dispatch('getMergeRequestVersions', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ }),
+ )
+ .then(() =>
+ store.dispatch('getMergeRequestChanges', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ }),
+ )
+ .then(mrChanges => {
+ mrChanges.changes.forEach((change, ind) => {
+ const changeTreeEntry = store.state.entries[change.new_path];
+
+ if (changeTreeEntry) {
+ store.dispatch('setFileMrChange', {
+ file: changeTreeEntry,
+ mrChange: change,
+ });
+
+ if (ind < 10) {
+ store.dispatch('getFileData', {
+ path: change.new_path,
+ makeFileActive: ind === 0,
+ });
+ }
+ }
+ });
+ })
+ .catch(e => {
+ flash('Error while loading the merge request. Please try again.');
+ throw e;
+ });
}
})
.catch(e => {
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index ef395b276af..f3c052d6903 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -21,6 +21,15 @@ export default class Model {
new this.monaco.Uri(null, null, this.file.path),
)),
);
+ if (this.file.mrChange) {
+ this.disposable.add(
+ (this.baseModel = this.monaco.editor.createModel(
+ this.file.baseRaw,
+ undefined,
+ new this.monaco.Uri(null, null, `target/${this.file.path}`),
+ )),
+ );
+ }
this.events = new Map();
@@ -28,10 +37,7 @@ export default class Model {
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
- eventHub.$on(
- `editor.update.model.content.${this.file.path}`,
- this.updateContent,
- );
+ eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
get url() {
@@ -58,6 +64,10 @@ export default class Model {
return this.originalModel;
}
+ getBaseModel() {
+ return this.baseModel;
+ }
+
setValue(value) {
this.getModel().setValue(value);
}
@@ -81,13 +91,7 @@ export default class Model {
this.disposable.dispose();
this.events.clear();
- eventHub.$off(
- `editor.update.model.dispose.${this.file.path}`,
- this.dispose,
- );
- eventHub.$off(
- `editor.update.model.content.${this.file.path}`,
- this.updateContent,
- );
+ eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
+ eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
}
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 887dd7e39b1..6b4ba30e086 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -109,11 +109,19 @@ export default class Editor {
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
+ attachMergeRequestModel(model) {
+ this.instance.setModel({
+ original: model.getBaseModel(),
+ modified: model.getModel(),
+ });
+
+ this.monaco.editor.createDiffNavigator(this.instance, {
+ alwaysRevealFirst: true,
+ });
+ }
+
setupMonacoTheme() {
- this.monaco.editor.defineTheme(
- gitlabTheme.themeName,
- gitlabTheme.monacoTheme,
- );
+ this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.setTheme('gitlab');
}
@@ -161,8 +169,6 @@ export default class Editor {
onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
- this.disposable.add(
- this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
- );
+ this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
}
}
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 5f1fb6cf843..a12e637616a 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -20,12 +20,35 @@ export default {
return Promise.resolve(file.raw);
}
- return Vue.http.get(file.rawPath, { params: { format: 'json' } })
+ return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
+ },
+ getBaseRawFileData(file, sha) {
+ if (file.tempFile) {
+ return Promise.resolve(file.baseRaw);
+ }
+
+ if (file.baseRaw) {
+ return Promise.resolve(file.baseRaw);
+ }
+
+ return Vue.http
+ .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
+ params: { format: 'json' },
+ })
.then(res => res.text());
},
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
+ getProjectMergeRequestData(projectId, mergeRequestId) {
+ return Api.mergeRequest(projectId, mergeRequestId);
+ },
+ getProjectMergeRequestChanges(projectId, mergeRequestId) {
+ return Api.mergeRequestChanges(projectId, mergeRequestId);
+ },
+ getProjectMergeRequestVersions(projectId, mergeRequestId) {
+ return Api.mergeRequestVersions(projectId, mergeRequestId);
+ },
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
},
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 880fa9ab8f7..3e512e79bba 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -7,8 +7,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url);
-export const setInitialData = ({ commit }, data) =>
- commit(types.SET_INITIAL_DATA, data);
+export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach(file => {
@@ -60,14 +59,11 @@ export const createTempEntry = (
) =>
new Promise(resolve => {
const worker = new FilesDecoratorWorker();
- const fullName =
- name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
+ const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) {
flash(
- `The name "${name
- .split('/')
- .pop()}" is already taken in this directory.`,
+ `The name "${name.split('/').pop()}" is already taken in this directory.`,
'alert',
document,
null,
@@ -144,3 +140,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
+export * from './actions/merge_request';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 0615f0fe90b..470d168e3e3 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -46,53 +46,63 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
commit(types.SET_CURRENT_BRANCH, file.branchId);
};
-export const getFileData = ({ state, commit, dispatch }, file) => {
+export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
+ const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
-
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
setPageTitle(pageTitle);
return res.json();
})
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
- commit(types.TOGGLE_FILE_OPEN, file.path);
- dispatch('setFileActive', file.path);
+ commit(types.TOGGLE_FILE_OPEN, path);
+ if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
- flash(
- 'Error loading file data. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- );
+ flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
});
};
-export const getRawFileData = ({ commit, dispatch }, file) =>
- service
- .getRawFileData(file)
- .then(raw => {
- commit(types.SET_FILE_RAW_DATA, { file, raw });
- })
- .catch(() =>
- flash(
- 'Error loading file content. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- ),
- );
+export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
+ commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
+};
+
+export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
+ const file = state.entries[path];
+ return new Promise((resolve, reject) => {
+ service
+ .getRawFileData(file)
+ .then(raw => {
+ commit(types.SET_FILE_RAW_DATA, { file, raw });
+ if (file.mrChange && file.mrChange.new_file === false) {
+ service
+ .getBaseRawFileData(file, baseSha)
+ .then(baseRaw => {
+ commit(types.SET_FILE_BASE_RAW_DATA, {
+ file,
+ baseRaw,
+ });
+ resolve(raw);
+ })
+ .catch(e => {
+ reject(e);
+ });
+ } else {
+ resolve(raw);
+ }
+ })
+ .catch(() => {
+ flash('Error loading file content. Please try again.');
+ reject();
+ });
+ });
+};
export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path];
@@ -119,10 +129,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => {
}
};
-export const setEditorPosition = (
- { getters, commit },
- { editorRow, editorColumn },
-) => {
+export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
if (getters.activeFile) {
commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
new file mode 100644
index 00000000000..da73034fd7d
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -0,0 +1,84 @@
+import flash from '~/flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+
+export const getMergeRequestData = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
+ service
+ .getProjectMergeRequestData(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST, {
+ projectPath: projectId,
+ mergeRequestId,
+ mergeRequest: data,
+ });
+ if (!state.currentMergeRequestId) {
+ commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
+ }
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request data. Please try again.');
+ reject(new Error(`Merge Request not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
+ }
+ });
+
+export const getMergeRequestChanges = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
+ service
+ .getProjectMergeRequestChanges(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST_CHANGES, {
+ projectPath: projectId,
+ mergeRequestId,
+ changes: data,
+ });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request changes. Please try again.');
+ reject(new Error(`Merge Request Changes not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
+ }
+ });
+
+export const getMergeRequestVersions = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
+ service
+ .getProjectMergeRequestVersions(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST_VERSIONS, {
+ projectPath: projectId,
+ mergeRequestId,
+ versions: data,
+ });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request versions. Please try again.');
+ reject(new Error(`Merge Request Versions not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 70a969a0325..6536be04f0a 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
-import {
- findEntry,
-} from '../utils';
+import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('setFileActive', row.path);
} else {
- dispatch('getFileData', row);
+ dispatch('getFileData', { path: row.path });
}
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
- service.getTreeLastCommit(tree.lastCommitPath)
- .then((res) => {
+ service
+ .getTreeLastCommit(tree.lastCommitPath)
+ .then(res => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
- .then((data) => {
- data.forEach((lastCommit) => {
+ .then(data => {
+ data.forEach(lastCommit => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) {
@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
-export const getFiles = (
- { state, commit, dispatch },
- { projectId, branchId } = {},
-) => new Promise((resolve, reject) => {
- if (!state.trees[`${projectId}/${branchId}`]) {
- const selectedProject = state.projects[projectId];
- commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
-
- service
- .getFiles(selectedProject.web_url, branchId)
- .then(res => res.json())
- .then((data) => {
- const worker = new FilesDecoratorWorker();
- worker.addEventListener('message', (e) => {
- const { entries, treeList } = e.data;
- const selectedTree = state.trees[`${projectId}/${branchId}`];
-
- commit(types.SET_ENTRIES, entries);
- commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
- commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
-
- worker.terminate();
-
- resolve();
- });
-
- worker.postMessage({
- data,
- projectId,
- branchId,
+export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
+ new Promise((resolve, reject) => {
+ if (!state.trees[`${projectId}/${branchId}`]) {
+ const selectedProject = state.projects[projectId];
+ commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
+
+ service
+ .getFiles(selectedProject.web_url, branchId)
+ .then(res => res.json())
+ .then(data => {
+ const worker = new FilesDecoratorWorker();
+ worker.addEventListener('message', e => {
+ const { entries, treeList } = e.data;
+ const selectedTree = state.trees[`${projectId}/${branchId}`];
+
+ commit(types.SET_ENTRIES, entries);
+ commit(types.SET_DIRECTORY_DATA, {
+ treePath: `${projectId}/${branchId}`,
+ data: treeList,
+ });
+ commit(types.TOGGLE_LOADING, {
+ entry: selectedTree,
+ forceValue: false,
+ });
+
+ worker.terminate();
+
+ resolve();
+ });
+
+ worker.postMessage({
+ data,
+ projectId,
+ branchId,
+ });
+ })
+ .catch(e => {
+ flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
+ reject(e);
});
- })
- .catch((e) => {
- flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
- reject(e);
- });
- } else {
- resolve();
- }
-});
-
+ } else {
+ resolve();
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 72902146f64..9413698db34 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,10 +1,8 @@
-export const activeFile = state =>
- state.openFiles.find(file => file.active) || null;
+export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
-export const modifiedFiles = state =>
- state.changedFiles.filter(f => !f.tempFile);
+export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => {
@@ -23,13 +21,21 @@ export const projectsWithTrees = state =>
};
});
+export const currentMergeRequest = state => {
+ if (state.projects[state.currentProjectId]) {
+ return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
+ }
+ return null;
+};
+
// eslint-disable-next-line no-confusing-arrow
export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
-export const hasChanges = state =>
- !!state.changedFiles.length || !!state.stagedFiles.length;
+export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
// eslint-disable-next-line no-confusing-arrow
export const collapseButtonTooltip = state =>
state.rightPanelCollapsed ? 'Expand sidebar' : 'Collapse sidebar';
+
+export const hasMergeRequest = state => !!state.currentMergeRequestId;
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 8be9f6a86e4..118b4df33f8 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
+// Merge Request Mutation Types
+export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
+export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
+export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
+export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
+
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
+export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
@@ -39,6 +46,7 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
+export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 2a165f778ae..25ce3e70781 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,5 +1,6 @@
import * as types from './mutation_types';
import projectMutations from './mutations/project';
+import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
@@ -11,10 +12,7 @@ export default {
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) {
Object.assign(state.entries[entry.path], {
- loading:
- forceValue !== undefined
- ? forceValue
- : !state.entries[entry.path].loading,
+ loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, {
@@ -88,9 +86,7 @@ export default {
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
- tree: state.trees[`${projectId}/${branchId}`].tree.concat(
- data.treeList,
- ),
+ tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
});
}
},
@@ -120,6 +116,7 @@ export default {
});
},
...projectMutations,
+ ...mergeRequestMutation,
...fileMutations,
...treeMutations,
...branchMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 78cae136476..f4a9354b024 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -28,6 +28,8 @@ export default {
rawPath: data.raw_path,
binary: data.binary,
renderError: data.render_error,
+ raw: null,
+ baseRaw: null,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
@@ -35,6 +37,11 @@ export default {
raw,
});
},
+ [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
+ Object.assign(state.entries[file.path], {
+ baseRaw,
+ });
+ },
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
@@ -59,6 +66,11 @@ export default {
editorColumn,
});
},
+ [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
+ Object.assign(state.entries[file.path], {
+ mrChange,
+ });
+ },
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
diff --git a/app/assets/javascripts/ide/stores/mutations/merge_request.js b/app/assets/javascripts/ide/stores/mutations/merge_request.js
new file mode 100644
index 00000000000..334819fe702
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/merge_request.js
@@ -0,0 +1,33 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
+ Object.assign(state, {
+ currentMergeRequestId,
+ });
+ },
+ [types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) {
+ Object.assign(state.projects[projectPath], {
+ mergeRequests: {
+ [mergeRequestId]: {
+ ...mergeRequest,
+ active: true,
+ changes: [],
+ versions: [],
+ baseCommitSha: null,
+ },
+ },
+ });
+ },
+ [types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) {
+ Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+ changes,
+ });
+ },
+ [types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) {
+ Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+ versions,
+ baseCommitSha: versions.length ? versions[0].base_commit_sha : null,
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 2816562a919..284b39a2c72 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -11,6 +11,7 @@ export default {
Object.assign(project, {
tree: [],
branches: {},
+ mergeRequests: {},
active: true,
});
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 6c09324e4ba..34975ac3144 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,6 +1,7 @@
export default () => ({
currentProjectId: '',
currentBranchId: '',
+ currentMergeRequestId: '',
changedFiles: [],
stagedFiles: [],
endpoints: {},
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index a266bb6771f..dd17544b656 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -51,7 +51,7 @@ export function removeParams(params) {
const url = document.createElement('a');
url.href = window.location.href;
- params.forEach((param) => {
+ params.forEach(param => {
url.search = removeParamQueryString(url.search, param);
});
@@ -83,3 +83,11 @@ export function refreshCurrentPage() {
export function redirectTo(url) {
return window.location.assign(url);
}
+
+export function webIDEUrl(route = undefined) {
+ let returnUrl = `${gon.relative_url_root}/-/ide/`;
+ if (route) {
+ returnUrl += `project${route}`;
+ }
+ return returnUrl;
+}
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 8a86c409b62..ceb02309959 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,59 +1,73 @@
<script>
- import Flash from '../../../flash';
- import editForm from './edit_form.vue';
- import Icon from '../../../vue_shared/components/icon.vue';
- import { __ } from '../../../locale';
+import Flash from '../../../flash';
+import editForm from './edit_form.vue';
+import Icon from '../../../vue_shared/components/icon.vue';
+import { __ } from '../../../locale';
+import eventHub from '../../event_hub';
- export default {
- components: {
- editForm,
- Icon,
+export default {
+ components: {
+ editForm,
+ Icon,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
},
- props: {
- isConfidential: {
- required: true,
- type: Boolean,
- },
- isEditable: {
- required: true,
- type: Boolean,
- },
- service: {
- required: true,
- type: Object,
- },
+ isEditable: {
+ required: true,
+ type: Boolean,
},
- data() {
- return {
- edit: false,
- };
+ service: {
+ required: true,
+ type: Object,
},
- computed: {
- confidentialityIcon() {
- return this.isConfidential ? 'eye-slash' : 'eye';
- },
+ },
+ data() {
+ return {
+ edit: false,
+ };
+ },
+ computed: {
+ confidentialityIcon() {
+ return this.isConfidential ? 'eye-slash' : 'eye';
},
- methods: {
- toggleForm() {
- this.edit = !this.edit;
- },
- updateConfidentialAttribute(confidential) {
- this.service.update('issue', { confidential })
- .then(() => location.reload())
- .catch(() => {
- Flash(__('Something went wrong trying to change the confidentiality of this issue'));
- });
- },
+ },
+ created() {
+ eventHub.$on('closeConfidentialityForm', this.toggleForm);
+ },
+ beforeDestroy() {
+ eventHub.$off('closeConfidentialityForm', this.toggleForm);
+ },
+ methods: {
+ toggleForm() {
+ this.edit = !this.edit;
},
- };
+ updateConfidentialAttribute(confidential) {
+ this.service
+ .update('issue', { confidential })
+ .then(() => location.reload())
+ .catch(() => {
+ Flash(
+ __(
+ 'Something went wrong trying to change the confidentiality of this issue',
+ ),
+ );
+ });
+ },
+ },
+};
</script>
<template>
<div class="block issuable-sidebar-item confidentiality">
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ @click="toggleForm"
+ >
<icon
:name="confidentialityIcon"
- :size="16"
aria-hidden="true"
/>
</div>
@@ -71,7 +85,6 @@
<div class="value sidebar-item-value hide-collapsed">
<editForm
v-if="edit"
- :toggle-form="toggleForm"
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index c569843b05f..3783f71a848 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -1,34 +1,34 @@
<script>
- import editFormButtons from './edit_form_buttons.vue';
- import { s__ } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
+import { s__ } from '../../../locale';
- export default {
- components: {
- editFormButtons,
+export default {
+ components: {
+ editFormButtons,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
},
- props: {
- isConfidential: {
- required: true,
- type: Boolean,
- },
- toggleForm: {
- required: true,
- type: Function,
- },
- updateConfidentialAttribute: {
- required: true,
- type: Function,
- },
+ updateConfidentialAttribute: {
+ required: true,
+ type: Function,
},
- computed: {
- confidentialityOnWarning() {
- return s__('confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.');
- },
- confidentialityOffWarning() {
- return s__('confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.');
- },
+ },
+ computed: {
+ confidentialityOnWarning() {
+ return s__(
+ 'confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.',
+ );
},
- };
+ confidentialityOffWarning() {
+ return s__(
+ 'confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.',
+ );
+ },
+ },
+};
</script>
<template>
@@ -45,7 +45,6 @@
</p>
<edit-form-buttons
:is-confidential="isConfidential"
- :toggle-form="toggleForm"
:update-confidential-attribute="updateConfidentialAttribute"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 49d5dfeea1a..38b1ddbfd5b 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -1,14 +1,13 @@
<script>
+import $ from 'jquery';
+import eventHub from '../../event_hub';
+
export default {
props: {
isConfidential: {
required: true,
type: Boolean,
},
- toggleForm: {
- required: true,
- type: Function,
- },
updateConfidentialAttribute: {
required: true,
type: Function,
@@ -22,6 +21,16 @@ export default {
return !this.isConfidential;
},
},
+ methods: {
+ closeForm() {
+ eventHub.$emit('closeConfidentialityForm');
+ $(this.$el).trigger('hidden.gl.dropdown');
+ },
+ submitForm() {
+ this.closeForm();
+ this.updateConfidentialAttribute(this.updateConfidentialBool);
+ },
+ },
};
</script>
@@ -30,14 +39,14 @@ export default {
<button
type="button"
class="btn btn-default append-right-10"
- @click="toggleForm"
+ @click="closeForm"
>
{{ __('Cancel') }}
</button>
<button
type="button"
class="btn btn-close"
- @click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
+ @click.prevent="submitForm"
>
{{ toggleButtonText }}
</button>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index bc32e974bc3..e1e4715826a 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -1,40 +1,43 @@
<script>
- import editFormButtons from './edit_form_buttons.vue';
- import issuableMixin from '../../../vue_shared/mixins/issuable';
- import { __, sprintf } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+import { __, sprintf } from '../../../locale';
- export default {
- components: {
- editFormButtons,
+export default {
+ components: {
+ editFormButtons,
+ },
+ mixins: [issuableMixin],
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
},
- mixins: [
- issuableMixin,
- ],
- props: {
- isLocked: {
- required: true,
- type: Boolean,
- },
- toggleForm: {
- required: true,
- type: Function,
- },
-
- updateLockedAttribute: {
- required: true,
- type: Function,
- },
+ updateLockedAttribute: {
+ required: true,
+ type: Function,
+ },
+ },
+ computed: {
+ lockWarning() {
+ return sprintf(
+ __(
+ 'Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.',
+ ),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
},
- computed: {
- lockWarning() {
- return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
- },
- unlockWarning() {
- return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
- },
+ unlockWarning() {
+ return sprintf(
+ __(
+ 'Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.',
+ ),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
},
- };
+ },
+};
</script>
<template>
@@ -54,7 +57,6 @@
<edit-form-buttons
:is-locked="isLocked"
- :toggle-form="toggleForm"
:update-locked-attribute="updateLockedAttribute"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index c3a553a7605..5e7b8f9698f 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,4 +1,7 @@
<script>
+import $ from 'jquery';
+import eventHub from '../../event_hub';
+
export default {
props: {
isLocked: {
@@ -6,11 +9,6 @@ export default {
type: Boolean,
},
- toggleForm: {
- required: true,
- type: Function,
- },
-
updateLockedAttribute: {
required: true,
type: Function,
@@ -26,6 +24,17 @@ export default {
return !this.isLocked;
},
},
+
+ methods: {
+ closeForm() {
+ eventHub.$emit('closeLockForm');
+ $(this.$el).trigger('hidden.gl.dropdown');
+ },
+ submitForm() {
+ this.closeForm();
+ this.updateLockedAttribute(this.toggleLock);
+ },
+ },
};
</script>
@@ -34,7 +43,7 @@ export default {
<button
type="button"
class="btn btn-default append-right-10"
- @click="toggleForm"
+ @click="closeForm"
>
{{ __('Cancel') }}
</button>
@@ -42,7 +51,7 @@ export default {
<button
type="button"
class="btn btn-close"
- @click.prevent="updateLockedAttribute(toggleLock)"
+ @click.prevent="submitForm"
>
{{ buttonText }}
</button>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index 0686910fc7e..e4893451af3 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,70 +1,93 @@
<script>
- import Flash from '~/flash';
- import editForm from './edit_form.vue';
- import issuableMixin from '../../../vue_shared/mixins/issuable';
- import Icon from '../../../vue_shared/components/icon.vue';
+import Flash from '~/flash';
+import editForm from './edit_form.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+import Icon from '../../../vue_shared/components/icon.vue';
+import eventHub from '../../event_hub';
- export default {
- components: {
- editForm,
- Icon,
- },
- mixins: [
- issuableMixin,
- ],
+export default {
+ components: {
+ editForm,
+ Icon,
+ },
+ mixins: [issuableMixin],
- props: {
- isLocked: {
- required: true,
- type: Boolean,
- },
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
+ },
- isEditable: {
- required: true,
- type: Boolean,
- },
+ isEditable: {
+ required: true,
+ type: Boolean,
+ },
- mediator: {
- required: true,
- type: Object,
- validator(mediatorObject) {
- return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
- },
+ mediator: {
+ required: true,
+ type: Object,
+ validator(mediatorObject) {
+ return (
+ mediatorObject.service &&
+ mediatorObject.service.update &&
+ mediatorObject.store
+ );
},
},
+ },
- computed: {
- lockIcon() {
- return this.isLocked ? 'lock' : 'lock-open';
- },
+ computed: {
+ lockIcon() {
+ return this.isLocked ? 'lock' : 'lock-open';
+ },
- isLockDialogOpen() {
- return this.mediator.store.isLockDialogOpen;
- },
+ isLockDialogOpen() {
+ return this.mediator.store.isLockDialogOpen;
},
+ },
- methods: {
- toggleForm() {
- this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
- },
+ created() {
+ eventHub.$on('closeLockForm', this.toggleForm);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('closeLockForm', this.toggleForm);
+ },
- updateLockedAttribute(locked) {
- this.mediator.service.update(this.issuableType, {
+ methods: {
+ toggleForm() {
+ this.mediator.store.isLockDialogOpen = !this.mediator.store
+ .isLockDialogOpen;
+ },
+
+ updateLockedAttribute(locked) {
+ this.mediator.service
+ .update(this.issuableType, {
discussion_locked: locked,
})
.then(() => location.reload())
- .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`)));
- },
+ .catch(() =>
+ Flash(
+ this.__(
+ `Something went wrong trying to change the locked state of this ${
+ this.issuableDisplayName
+ }`,
+ ),
+ ),
+ );
},
- };
+ },
+};
</script>
<template>
<div class="block issuable-sidebar-item lock">
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ @click="toggleForm"
+ >
<icon
:name="lockIcon"
- :size="16"
aria-hidden="true"
class="sidebar-item-icon is-active"
/>
@@ -85,7 +108,6 @@
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="isLockDialogOpen"
- :toggle-form="toggleForm"
:is-locked="isLocked"
:update-locked-attribute="updateLockedAttribute"
:issuable-type="issuableType"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 3d886e7d628..18ee4c62bf1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,53 +1,57 @@
<script>
- import tooltip from '~/vue_shared/directives/tooltip';
- import { n__ } from '~/locale';
- import icon from '~/vue_shared/components/icon.vue';
- import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { n__ } from '~/locale';
+import { webIDEUrl } from '~/lib/utils/url_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
- export default {
- name: 'MRWidgetHeader',
- directives: {
- tooltip,
+export default {
+ name: 'MRWidgetHeader',
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ clipboardButton,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
},
- components: {
- icon,
- clipboardButton,
+ },
+ computed: {
+ shouldShowCommitsBehindText() {
+ return this.mr.divergedCommitsCount > 0;
},
- props: {
- mr: {
- type: Object,
- required: true,
- },
+ commitsText() {
+ return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
},
- computed: {
- shouldShowCommitsBehindText() {
- return this.mr.divergedCommitsCount > 0;
- },
- commitsText() {
- return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
- },
- branchNameClipboardData() {
- // This supports code in app/assets/javascripts/copy_to_clipboard.js that
- // works around ClipboardJS limitations to allow the context-specific
- // copy/pasting of plain text or GFM.
- return JSON.stringify({
- text: this.mr.sourceBranch,
- gfm: `\`${this.mr.sourceBranch}\``,
- });
- },
- isSourceBranchLong() {
- return this.isBranchTitleLong(this.mr.sourceBranch);
- },
- isTargetBranchLong() {
- return this.isBranchTitleLong(this.mr.targetBranch);
- },
+ branchNameClipboardData() {
+ // This supports code in app/assets/javascripts/copy_to_clipboard.js that
+ // works around ClipboardJS limitations to allow the context-specific
+ // copy/pasting of plain text or GFM.
+ return JSON.stringify({
+ text: this.mr.sourceBranch,
+ gfm: `\`${this.mr.sourceBranch}\``,
+ });
},
- methods: {
- isBranchTitleLong(branchTitle) {
- return branchTitle.length > 32;
- },
+ isSourceBranchLong() {
+ return this.isBranchTitleLong(this.mr.sourceBranch);
},
- };
+ isTargetBranchLong() {
+ return this.isBranchTitleLong(this.mr.targetBranch);
+ },
+ webIdePath() {
+ return webIDEUrl(this.mr.statusPath.replace('.json', ''));
+ },
+ },
+ methods: {
+ isBranchTitleLong(branchTitle) {
+ return branchTitle.length > 32;
+ },
+ },
+};
</script>
<template>
<div class="mr-source-target">
@@ -96,6 +100,13 @@
</div>
<div v-if="mr.isOpen">
+ <a
+ v-if="!mr.sourceBranchRemoved"
+ :href="webIdePath"
+ class="btn btn-sm btn-default inline js-web-ide"
+ >
+ {{ s__("mrWidget|Web IDE") }}
+ </a>
<button
data-target="#modal_merge_info"
data-toggle="modal"
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3dd4a613789..798f248dad4 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -88,7 +88,6 @@
.right-sidebar {
border-left: 1px solid $border-color;
- height: calc(100% - #{$header-height});
}
.with-performance-bar .right-sidebar.affix {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index e21a9f0afc9..2c0ed976301 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -522,10 +522,6 @@
.with-performance-bar .right-sidebar {
top: $header-height + $performance-bar-height;
-
- .issuable-sidebar {
- height: calc(100% - #{$performance-bar-height});
- }
}
.sidebar-move-issue-confirmation-button {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index cf8f6202611..8f79ea90cc5 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -19,7 +19,7 @@
.ide-view {
display: flex;
height: calc(100vh - #{$header-height});
- margin-top: 40px;
+ margin-top: 0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
@@ -53,6 +53,7 @@
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
+ max-width: inherit;
svg {
vertical-align: middle;
@@ -460,6 +461,8 @@
display: flex;
flex-direction: column;
flex: 1;
+ max-height: 100%;
+ overflow: auto;
}
.ide-commity-empty-state {
@@ -474,7 +477,7 @@
.multi-file-commit-panel-header {
display: flex;
align-items: center;
- margin-bottom: 12px;
+ margin-bottom: 0;
border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0;
@@ -689,8 +692,14 @@
overflow: hidden;
&.nav-only {
+ padding-top: $header-height;
+
+ .with-performance-bar & {
+ padding-top: $header-height + $performance-bar-height;
+ }
+
.flash-container {
- margin-top: $header-height;
+ margin-top: 0;
margin-bottom: 0;
}
@@ -700,7 +709,7 @@
}
.content-wrapper {
- margin-top: $header-height;
+ margin-top: 0;
padding-bottom: 0;
}
@@ -724,11 +733,11 @@
.with-performance-bar .ide.nav-only {
.flash-container {
- margin-top: #{$header-height + $performance-bar-height};
+ margin-top: 0;
}
.content-wrapper {
- margin-top: #{$header-height + $performance-bar-height};
+ margin-top: 0;
padding-bottom: 0;
}
@@ -737,10 +746,6 @@
}
&.flash-shown {
- .content-wrapper {
- margin-top: 0;
- }
-
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
}
diff --git a/app/assets/stylesheets/pages/repo.scss.orig b/app/assets/stylesheets/pages/repo.scss.orig
new file mode 100644
index 00000000000..57b995adb64
--- /dev/null
+++ b/app/assets/stylesheets/pages/repo.scss.orig
@@ -0,0 +1,786 @@
+.project-refs-form,
+.project-refs-target-form {
+ display: inline-block;
+}
+
+.fade-enter,
+.fade-leave-to {
+ opacity: 0;
+}
+
+.commit-message {
+ @include str-truncated(250px);
+}
+
+.editable-mode {
+ display: inline-block;
+}
+
+.ide-view {
+ display: flex;
+ height: calc(100vh - #{$header-height});
+ margin-top: 40px;
+ color: $almost-black;
+ border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+
+ &.is-collapsed {
+ .ide-file-list {
+ max-width: 250px;
+ }
+ }
+
+ .file-status-icon {
+ width: 10px;
+ height: 10px;
+ }
+}
+
+.ide-file-list {
+ flex: 1;
+
+ .file {
+ cursor: pointer;
+
+ &.file-open {
+ background: $white-normal;
+ }
+
+ .ide-file-name {
+ flex: 1;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ svg {
+ vertical-align: middle;
+ margin-right: 2px;
+ }
+
+ .loading-container {
+ margin-right: 4px;
+ display: inline-block;
+ }
+ }
+
+ .ide-file-changed-icon {
+ margin-left: auto;
+ }
+
+ .ide-new-btn {
+ display: none;
+ margin-bottom: -4px;
+ margin-right: -8px;
+ }
+
+ &:hover {
+ .ide-new-btn {
+ display: block;
+ }
+ }
+
+ &.folder {
+ svg {
+ fill: $gl-text-color-secondary;
+ }
+ }
+ }
+
+ a {
+ color: $gl-text-color;
+ }
+
+ th {
+ position: sticky;
+ top: 0;
+ }
+}
+
+.file-name,
+.file-col-commit-message {
+ display: flex;
+ overflow: visible;
+ padding: 6px 12px;
+}
+
+.multi-file-loading-container {
+ margin-top: 10px;
+ padding: 10px;
+
+ .animation-container {
+ background: $gray-light;
+
+ div {
+ background: $gray-light;
+ }
+ }
+}
+
+.multi-file-table-col-commit-message {
+ white-space: nowrap;
+ width: 50%;
+}
+
+.multi-file-edit-pane {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ border-left: 1px solid $white-dark;
+ overflow: hidden;
+}
+
+.multi-file-tabs {
+ display: flex;
+ background-color: $white-normal;
+ box-shadow: inset 0 -1px $white-dark;
+
+ > ul {
+ display: flex;
+ overflow-x: auto;
+ }
+
+ li {
+ position: relative;
+ }
+
+ .dropdown {
+ display: flex;
+ margin-left: auto;
+ margin-bottom: 1px;
+ padding: 0 $grid-size;
+ border-left: 1px solid $white-dark;
+ background-color: $white-light;
+
+ &.shadow {
+ box-shadow: 0 0 10px $dropdown-shadow-color;
+ }
+
+ .btn {
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+ }
+}
+
+.multi-file-tab {
+ @include str-truncated(150px);
+ padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
+ background-color: $gray-normal;
+ border-right: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ cursor: pointer;
+
+ svg {
+ vertical-align: middle;
+ }
+
+ &.active {
+ background-color: $white-light;
+ border-bottom-color: $white-light;
+ }
+}
+
+.multi-file-tab-close {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ width: 16px;
+ height: 16px;
+ padding: 0;
+ background: none;
+ border: 0;
+ border-radius: $border-radius-default;
+ color: $theme-gray-900;
+ transform: translateY(-50%);
+
+ svg {
+ position: relative;
+ top: -1px;
+ }
+
+ &:hover {
+ background-color: $theme-gray-200;
+ }
+
+ &:focus {
+ background-color: $blue-500;
+ color: $white-light;
+ outline: 0;
+
+ svg {
+ fill: currentColor;
+ }
+ }
+}
+
+.multi-file-edit-pane-content {
+ flex: 1;
+ height: 0;
+}
+
+.blob-editor-container {
+ flex: 1;
+ height: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .vertical-center {
+ min-height: auto;
+ }
+
+ .monaco-editor .lines-content .cigr {
+ display: none;
+ }
+
+ .monaco-diff-editor.vs {
+ .editor.modified {
+ box-shadow: none;
+ }
+
+ .diagonal-fill {
+ display: none !important;
+ }
+
+ .diffOverview {
+ background-color: $white-light;
+ border-left: 1px solid $white-dark;
+ cursor: ns-resize;
+ }
+
+ .diffViewport {
+ display: none;
+ }
+
+ .char-insert {
+ background-color: $line-added-dark;
+ }
+
+ .char-delete {
+ background-color: $line-removed-dark;
+ }
+
+ .line-numbers {
+ color: $black-transparent;
+ }
+
+ .view-overlays {
+ .line-insert {
+ background-color: $line-added;
+ }
+
+ .line-delete {
+ background-color: $line-removed;
+ }
+ }
+
+ .margin {
+ background-color: $gray-light;
+ border-right: 1px solid $white-normal;
+
+ .line-insert {
+ border-right: 1px solid $line-added-dark;
+ }
+
+ .line-delete {
+ border-right: 1px solid $line-removed-dark;
+ }
+ }
+
+ .margin-view-overlays .insert-sign,
+ .margin-view-overlays .delete-sign {
+ opacity: 0.4;
+ }
+
+ .cursors-layer {
+ display: none;
+ }
+ }
+}
+
+.multi-file-editor-holder {
+ height: 100%;
+}
+
+.multi-file-editor-btn-group {
+ padding: $gl-bar-padding $gl-padding;
+ border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ background: $white-light;
+}
+
+.ide-status-bar {
+ padding: $gl-bar-padding $gl-padding;
+ background: $white-light;
+ display: flex;
+ justify-content: space-between;
+
+ svg {
+ vertical-align: middle;
+ }
+}
+
+// Not great, but this is to deal with our current output
+.multi-file-preview-holder {
+ height: 100%;
+ overflow: scroll;
+
+ .file-content.code {
+ display: flex;
+
+ i {
+ margin-left: -10px;
+ }
+ }
+
+ .line-numbers {
+ min-width: 50px;
+ }
+
+ .file-content,
+ .line-numbers,
+ .blob-content,
+ .code {
+ min-height: 100%;
+ }
+}
+
+.file-content.blob-no-preview {
+ a {
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
+.multi-file-commit-panel {
+ display: flex;
+ position: relative;
+ flex-direction: column;
+ width: 340px;
+ padding: 0;
+ background-color: $gray-light;
+ padding-right: 3px;
+
+ .projects-sidebar {
+ display: flex;
+ flex-direction: column;
+
+ .context-header {
+ width: auto;
+ margin-right: 0;
+ }
+ }
+
+ .multi-file-commit-panel-inner {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ }
+
+ .multi-file-commit-panel-inner-scroll {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: auto;
+ }
+
+ &.is-collapsed {
+ width: 60px;
+
+ .multi-file-commit-list {
+ padding-top: $gl-padding;
+ overflow: hidden;
+ }
+
+ .multi-file-context-bar-icon {
+ align-items: center;
+
+ svg {
+ float: none;
+ margin: 0;
+ }
+ }
+ }
+
+ .branch-container {
+ border-left: 4px solid $indigo-700;
+ margin-bottom: $gl-bar-padding;
+ }
+
+ .branch-header {
+ background: $white-dark;
+ display: flex;
+ }
+
+ .branch-header-title {
+ flex: 1;
+ padding: $grid-size $gl-padding;
+ color: $indigo-700;
+ font-weight: $gl-font-weight-bold;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+
+ .branch-header-btns {
+ padding: $gl-vert-padding $gl-padding;
+ }
+
+ .left-collapse-btn {
+ display: none;
+ background: $gray-light;
+ text-align: left;
+ border-top: 1px solid $white-dark;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+}
+
+.multi-file-context-bar-icon {
+ padding: 10px;
+
+ svg {
+ margin-right: 10px;
+ float: left;
+ }
+}
+
+.multi-file-commit-panel-section {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
+
+.multi-file-commit-empty-state-container {
+ align-items: center;
+ justify-content: center;
+}
+
+.multi-file-commit-panel-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+ border-bottom: 1px solid $white-dark;
+ padding: $gl-btn-padding 0;
+
+ &.is-collapsed {
+ border-bottom: 1px solid $white-dark;
+
+ svg {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .multi-file-commit-panel-collapse-btn {
+ margin-right: auto;
+ margin-left: auto;
+ border-left: 0;
+ }
+ }
+}
+
+.multi-file-commit-panel-header-title {
+ display: flex;
+ flex: 1;
+ padding: 0 $gl-btn-padding;
+
+ svg {
+ margin-right: $gl-btn-padding;
+ }
+}
+
+.multi-file-commit-panel-collapse-btn {
+ border-left: 1px solid $white-dark;
+}
+
+.multi-file-commit-list {
+ flex: 1;
+ overflow: auto;
+ padding: $gl-padding 0;
+ min-height: 60px;
+}
+
+.multi-file-commit-list-item {
+ display: flex;
+ padding: 0;
+ align-items: center;
+
+ .multi-file-discard-btn {
+ display: none;
+ margin-left: auto;
+ color: $gl-link-color;
+ padding: 0 2px;
+
+ &:focus,
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ &:hover {
+ background: $white-normal;
+
+ .multi-file-discard-btn {
+ display: block;
+ }
+ }
+}
+
+.multi-file-addition {
+ fill: $green-500;
+}
+
+.multi-file-modified {
+ fill: $orange-500;
+}
+
+.multi-file-commit-list-collapsed {
+ display: flex;
+ flex-direction: column;
+
+ > svg {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .file-status-icon {
+ width: 10px;
+ height: 10px;
+ margin-left: 3px;
+ }
+}
+
+.multi-file-commit-list-path {
+ padding: $grid-size / 2;
+ padding-left: $gl-padding;
+ background: none;
+ border: 0;
+ text-align: left;
+ width: 100%;
+ min-width: 0;
+
+ svg {
+ min-width: 16px;
+ vertical-align: middle;
+ display: inline-block;
+ }
+
+ &:hover,
+ &:focus {
+ outline: 0;
+ }
+}
+
+.multi-file-commit-list-file-path {
+ @include str-truncated(100%);
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ &:active {
+ text-decoration: none;
+ }
+}
+
+.multi-file-commit-form {
+ padding: $gl-padding;
+ border-top: 1px solid $white-dark;
+
+ .btn {
+ font-size: $gl-font-size;
+ }
+}
+
+.multi-file-commit-message.form-control {
+ height: 160px;
+ resize: none;
+}
+
+.dirty-diff {
+ // !important need to override monaco inline style
+ width: 4px !important;
+ left: 0 !important;
+
+ &-modified {
+ background-color: $blue-500;
+ }
+
+ &-added {
+ background-color: $green-600;
+ }
+
+ &-removed {
+ height: 0 !important;
+ width: 0 !important;
+ bottom: -2px;
+ border-style: solid;
+ border-width: 5px;
+ border-color: transparent transparent transparent $red-500;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100px;
+ height: 1px;
+ background-color: rgba($red-500, 0.5);
+ }
+ }
+}
+
+.ide-loading {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.ide-empty-state {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.ide-new-btn {
+ .dropdown-toggle svg {
+ margin-top: -2px;
+ margin-bottom: 2px;
+ }
+
+ .dropdown-menu {
+ left: auto;
+ right: 0;
+
+ label {
+ font-weight: $gl-font-weight-normal;
+ padding: 5px 8px;
+ margin-bottom: 0;
+ }
+ }
+}
+
+.ide {
+ overflow: hidden;
+
+ &.nav-only {
+ .flash-container {
+ margin-top: $header-height;
+ margin-bottom: 0;
+ }
+
+ .alert-wrapper .flash-container .flash-alert:last-child,
+ .alert-wrapper .flash-container .flash-notice:last-child {
+ margin-bottom: 0;
+ }
+
+ .content-wrapper {
+ margin-top: $header-height;
+ padding-bottom: 0;
+ }
+
+ &.flash-shown {
+ .content-wrapper {
+ margin-top: 0;
+ }
+
+ .ide-view {
+ height: calc(100vh - #{$header-height + $flash-height});
+ }
+ }
+
+ .projects-sidebar {
+ .multi-file-commit-panel-inner-scroll {
+ flex: 1;
+ }
+ }
+ }
+}
+
+.with-performance-bar .ide.nav-only {
+ .flash-container {
+ margin-top: #{$header-height + $performance-bar-height};
+ }
+
+ .content-wrapper {
+ margin-top: #{$header-height + $performance-bar-height};
+ padding-bottom: 0;
+ }
+
+ .ide-view {
+ height: calc(100vh - #{$header-height + $performance-bar-height});
+ }
+
+ &.flash-shown {
+ .content-wrapper {
+ margin-top: 0;
+ }
+
+ .ide-view {
+ height: calc(
+ 100vh - #{$header-height + $performance-bar-height + $flash-height}
+ );
+ }
+ }
+}
+
+.dragHandle {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background-color: $white-dark;
+
+ &.dragright {
+ right: 0;
+ }
+
+ &.dragleft {
+ left: 0;
+ }
+}
+
+.ide-commit-radios {
+ label {
+ font-weight: normal;
+ }
+
+ .help-block {
+ margin-top: 0;
+ line-height: 0;
+ }
+}
+
+.ide-commit-new-branch {
+ margin-left: 25px;
+}
+
+.ide-external-links {
+ p {
+ margin: 0;
+ }
+}
+
+.ide-sidebar-link {
+ padding: $gl-padding-8 $gl-padding;
+ background: $indigo-700;
+ color: $white-light;
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+
+ &:focus,
+ &:hover {
+ color: $white-light;
+ text-decoration: underline;
+ background: $indigo-500;
+ }
+
+ &:active {
+ background: $indigo-800;
+ }
+}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index dd0b38970bd..ea302f17d16 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -50,9 +50,19 @@ class Admin::AppearancesController < Admin::ApplicationController
# Only allow a trusted parameter "white list" through.
def appearance_params
- params.require(:appearance).permit(
- :title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
- :new_project_guidelines, :updated_by
- )
+ params.require(:appearance).permit(allowed_appearance_params)
+ end
+
+ def allowed_appearance_params
+ %i[
+ title
+ description
+ logo
+ logo_cache
+ header_logo
+ header_logo_cache
+ new_project_guidelines
+ updated_by
+ ]
end
end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index f14cb5f6a9f..a5ea9ff7ed7 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -46,6 +46,8 @@ class Projects::ServicesController < Projects::ApplicationController
else
{ error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') }
end
+ rescue Gitlab::HTTP::BlockedUrlError => e
+ { error: true, message: 'Test failed.', service_response: e.message }
end
def success_message
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index c037de33c22..f48db024e3f 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,27 +1,27 @@
module AppearancesHelper
def brand_title
- brand_item&.title.presence || 'GitLab Community Edition'
+ current_appearance&.title.presence || 'GitLab Community Edition'
end
def brand_image
- image_tag(brand_item.logo) if brand_item&.logo?
+ image_tag(current_appearance.logo) if current_appearance&.logo?
end
def brand_text
- markdown_field(brand_item, :description)
+ markdown_field(current_appearance, :description)
end
def brand_new_project_guidelines
- markdown_field(brand_item, :new_project_guidelines)
+ markdown_field(current_appearance, :new_project_guidelines)
end
- def brand_item
+ def current_appearance
@appearance ||= Appearance.current
end
def brand_header_logo
- if brand_item&.header_logo?
- image_tag brand_item.header_logo
+ if current_appearance&.header_logo?
+ image_tag current_appearance.header_logo
else
render 'shared/logo.svg'
end
@@ -29,7 +29,7 @@ module AppearancesHelper
# Skip the 'GitLab' type logo when custom brand logo is set
def brand_header_logo_type
- unless brand_item&.header_logo?
+ unless current_appearance&.header_logo?
render 'shared/logo_type.svg'
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 701be97ee96..86ec500ceb3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -285,6 +285,10 @@ module ApplicationHelper
class_names
end
+ # EE feature: System header and footer, unavailable in CE
+ def system_message_class
+ end
+
# Returns active css class when condition returns true
# otherwise returns nil.
#
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 4ddc1dbed49..c86a26ac30f 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -54,9 +54,9 @@ module EmailsHelper
end
def header_logo
- if brand_item && brand_item.header_logo?
+ if current_appearance&.header_logo?
image_tag(
- brand_item.header_logo,
+ current_appearance.header_logo,
style: 'height: 50px'
)
else
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb
index ec56cc53aea..760f01f225b 100644
--- a/app/models/ci/artifact_blob.rb
+++ b/app/models/ci/artifact_blob.rb
@@ -36,16 +36,15 @@ module Ci
def external_url(project, job)
return unless external_link?(job)
- full_path_parts = project.full_path_components
- top_level_group = full_path_parts.shift
+ url_project_path = project.full_path.partition('/').last
artifact_path = [
- '-', *full_path_parts, '-',
+ '-', url_project_path, '-',
'jobs', job.id,
'artifacts', path
].join('/')
- "#{pages_config.protocol}://#{top_level_group}.#{pages_config.host}/#{artifact_path}"
+ "#{project.pages_group_url}/#{artifact_path}"
end
def external_link?(job)
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index ed02af05e3d..18e96389199 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -6,6 +6,7 @@ module Ci
include ObjectStorage::BackgroundMove
include Presentable
include Importable
+ include Gitlab::Utils::StrongMemoize
MissingDependenciesError = Class.new(StandardError)
@@ -25,15 +26,17 @@ module Ci
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :metadata, class_name: 'Ci::BuildMetadata'
-
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
- # The "environment" field for builds is a String, and is the unexpanded name
+ ##
+ # The "environment" field for builds is a String, and is the unexpanded name!
+ #
def persisted_environment
- @persisted_environment ||= Environment.find_by(
- name: expanded_environment_name,
- project: project
- )
+ return unless has_environment?
+
+ strong_memoize(:persisted_environment) do
+ Environment.find_by(name: expanded_environment_name, project: project)
+ end
end
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
@@ -212,7 +215,11 @@ module Ci
end
def expanded_environment_name
- ExpandVariables.expand(environment, simple_variables) if environment
+ return unless has_environment?
+
+ strong_memoize(:expanded_environment_name) do
+ ExpandVariables.expand(environment, simple_variables)
+ end
end
def has_environment?
@@ -258,31 +265,52 @@ module Ci
Gitlab::Utils.slugify(ref.to_s)
end
- # Variables whose value does not depend on environment
- def simple_variables
- variables(environment: nil)
- end
-
- # All variables, including those dependent on environment, which could
- # contain unexpanded variables.
- def variables(environment: persisted_environment)
- collection = Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ ##
+ # Variables in the environment name scope.
+ #
+ def scoped_variables(environment: expanded_environment_name)
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables)
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runner
- variables.concat(project.deployment_variables(environment: environment)) if has_environment?
+ variables.concat(project.deployment_variables(environment: environment)) if environment
variables.concat(yaml_variables)
variables.concat(user_variables)
- variables.concat(project.group.secret_variables_for(ref, project)) if project.group
- variables.concat(secret_variables(environment: environment))
+ variables.concat(secret_group_variables)
+ variables.concat(secret_project_variables(environment: environment))
variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
- variables.concat(persisted_environment_variables) if environment
end
+ end
+
+ ##
+ # Variables that do not depend on the environment name.
+ #
+ def simple_variables
+ strong_memoize(:simple_variables) do
+ scoped_variables(environment: nil).to_runner_variables
+ end
+ end
- collection.to_runner_variables
+ ##
+ # All variables, including persisted environment variables.
+ #
+ def variables
+ Gitlab::Ci::Variables::Collection.new
+ .concat(persisted_variables)
+ .concat(scoped_variables)
+ .concat(persisted_environment_variables)
+ .to_runner_variables
+ end
+
+ ##
+ # Regular Ruby hash of scoped variables, without duplicates that are
+ # possible to be present in an array of hashes returned from `variables`.
+ #
+ def scoped_variables_hash
+ scoped_variables.to_hash
end
def features
@@ -459,9 +487,14 @@ module Ci
end
end
- def secret_variables(environment: persisted_environment)
+ def secret_group_variables
+ return [] unless project.group
+
+ project.group.secret_variables_for(ref, project)
+ end
+
+ def secret_project_variables(environment: persisted_environment)
project.secret_variables_for(ref: ref, environment: environment)
- .map(&:to_runner_variable)
end
def steps
@@ -558,6 +591,21 @@ module Ci
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
+ def persisted_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ return variables unless persisted?
+
+ variables
+ .append(key: 'CI_JOB_ID', value: id.to_s)
+ .append(key: 'CI_JOB_TOKEN', value: token, public: false)
+ .append(key: 'CI_BUILD_ID', value: id.to_s)
+ .append(key: 'CI_BUILD_TOKEN', value: token, public: false)
+ .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
+ .append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
+ .append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
+ end
+ end
+
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
@@ -566,16 +614,11 @@ module Ci
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
- variables.append(key: 'CI_JOB_ID', value: id.to_s)
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
- variables.append(key: 'CI_JOB_TOKEN', value: token, public: false)
variables.append(key: 'CI_COMMIT_SHA', value: sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
- variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
- variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
- variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
@@ -583,23 +626,8 @@ module Ci
end
end
- def persisted_environment_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless persisted_environment
-
- variables.concat(persisted_environment.predefined_variables)
-
- # Here we're passing unexpanded environment_url for runner to expand,
- # and we need to make sure that CI_ENVIRONMENT_NAME and
- # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
- variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
- end
- end
-
def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_BUILD_ID', value: id.to_s)
- variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
variables.append(key: 'CI_BUILD_REF', value: sha)
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
@@ -612,6 +640,19 @@ module Ci
end
end
+ def persisted_environment_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ return variables unless persisted? && persisted_environment.present?
+
+ variables.concat(persisted_environment.predefined_variables)
+
+ # Here we're passing unexpanded environment_url for runner to expand,
+ # and we need to make sure that CI_ENVIRONMENT_NAME and
+ # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
+ variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
+ end
+ end
+
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index bfdfc5ae6fe..77947d515c1 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -51,6 +51,10 @@ module Clusters
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
+ scope :user_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:user]) }
+ scope :gcp_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:gcp]) }
+ scope :gcp_installed, -> { gcp_provided.includes(:provider_gcp).where(cluster_providers_gcp: { status: ::Clusters::Providers::Gcp.state_machines[:status].states[:created].value }) }
+
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
def status_name
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 7b7c8eac773..8f3eb75bfa9 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -4,6 +4,8 @@ module Clusters
extend ActiveSupport::Concern
included do
+ scope :installed, -> { where(status: self.state_machines[:status].states[:installed].value) }
+
state_machine :status, initial: :not_installable do
state :not_installable, value: -2
state :errored, value: -1
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 7bfc45c1f43..6a94d60c828 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -23,6 +23,7 @@ class Issue < ActiveRecord::Base
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
+ belongs_to :closed_by, class_name: 'User'
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
@@ -78,6 +79,11 @@ class Issue < ActiveRecord::Base
before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now
end
+
+ before_transition closed: :opened do |issue|
+ issue.closed_at = nil
+ issue.closed_by = nil
+ end
end
class << self
diff --git a/app/models/project.rb b/app/models/project.rb
index 6a420663644..b343786d2c9 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1346,20 +1346,19 @@ class Project < ActiveRecord::Base
Dir.exist?(public_pages_path)
end
- def pages_url
- subdomain, _, url_path = full_path.partition('/')
-
- # The hostname always needs to be in downcased
- # All web servers convert hostname to lowercase
- host = "#{subdomain}.#{Settings.pages.host}".downcase
-
+ def pages_group_url
# The host in URL always needs to be downcased
- url = Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
- "#{prefix}#{subdomain}."
+ Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
+ "#{prefix}#{pages_subdomain}."
end.downcase
+ end
+
+ def pages_url
+ url = pages_group_url
+ url_path = full_path.partition('/').last
# If the project path is the same as host, we serve it as group page
- return url if host == url_path
+ return url if url == "#{Settings.pages.protocol}://#{url_path}"
"#{url}/#{url_path}"
end
@@ -1545,8 +1544,8 @@ class Project < ActiveRecord::Base
@errors = original_errors
end
- def add_export_job(current_user:, params: {})
- job_id = ProjectExportWorker.perform_async(current_user.id, self.id, params)
+ def add_export_job(current_user:, after_export_strategy: nil, params: {})
+ job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
if job_id
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
@@ -1572,6 +1571,8 @@ class Project < ActiveRecord::Base
def export_status
if export_in_progress?
:started
+ elsif after_export_in_progress?
+ :after_export_action
elsif export_project_path
:finished
else
@@ -1583,12 +1584,22 @@ class Project < ActiveRecord::Base
import_export_shared.active_export_count > 0
end
+ def after_export_in_progress?
+ import_export_shared.after_export_in_progress?
+ end
+
def remove_exports
return nil unless export_path.present?
FileUtils.rm_rf(export_path)
end
+ def remove_exported_project_file
+ return unless export_project_path.present?
+
+ FileUtils.rm_f(export_project_path)
+ end
+
def full_path_slug
Gitlab::Utils.slugify(full_path.to_s)
end
diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb
index 6d0dd0a9f99..9269b8d2620 100644
--- a/app/services/boards/list_service.rb
+++ b/app/services/boards/list_service.rb
@@ -2,11 +2,15 @@ module Boards
class ListService < Boards::BaseService
def execute
create_board! if parent.boards.empty?
- parent.boards
+ boards
end
private
+ def boards
+ parent.boards
+ end
+
def create_board!
Boards::CreateService.new(parent, current_user).execute
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 0c5cf2c62ad..fee5bc38f7b 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -23,6 +23,7 @@ module Issues
end
if project.issues_enabled? && issue.close
+ issue.update(closed_by: current_user)
event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note
notification_service.close_issue(issue, current_user) if notifications
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index d16aa3de639..402cddd3ec1 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -1,22 +1,36 @@
module Projects
module ImportExport
class ExportService < BaseService
- def execute(_options = {})
+ def execute(after_export_strategy = nil, options = {})
@shared = project.import_export_shared
- save_all
+
+ save_all!
+ execute_after_export_action(after_export_strategy)
end
private
- def save_all
- if [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+ def execute_after_export_action(after_export_strategy)
+ return unless after_export_strategy
+
+ unless after_export_strategy.execute(current_user, project)
+ cleanup_and_notify_error
+ end
+ end
+
+ def save_all!
+ if save_services
Gitlab::ImportExport::Saver.save(project: project, shared: @shared)
notify_success
else
- cleanup_and_notify
+ cleanup_and_notify_error!
end
end
+ def save_services
+ [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+ end
+
def version_saver
Gitlab::ImportExport::VersionSaver.new(shared: @shared)
end
@@ -41,19 +55,22 @@ module Projects
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end
- def cleanup_and_notify
+ def cleanup_and_notify_error
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
FileUtils.rm_rf(@shared.export_path)
notify_error
+ end
+
+ def cleanup_and_notify_error!
+ cleanup_and_notify_error
+
raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
end
def notify_success
Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported")
-
- notification_service.project_exported(@project, @current_user)
end
def notify_error
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index a34024f4f80..a3828acc50b 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -28,7 +28,11 @@ 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, valid_ports: Project::VALID_IMPORT_PORTS)
+ begin
+ Gitlab::UrlBlocker.validate!(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ raise Error, "Blocked import URL: #{e.message}"
+ end
end
# We should skip the repository for a GitHub import or GitLab project import,
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 5bf8208e035..9c8877be14e 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -178,6 +178,9 @@ module Projects
def latest_sha
project.commit(build.ref).try(:sha).to_s
+ ensure
+ # Close any file descriptors that were opened and free libgit2 buffers
+ project.cleanup
end
def sha
diff --git a/app/validators/certificate_fingerprint_validator.rb b/app/validators/certificate_fingerprint_validator.rb
new file mode 100644
index 00000000000..17df756183a
--- /dev/null
+++ b/app/validators/certificate_fingerprint_validator.rb
@@ -0,0 +1,9 @@
+class CertificateFingerprintValidator < ActiveModel::EachValidator
+ FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/.freeze
+
+ def validate_each(record, attribute, value)
+ unless value.try(:match, FINGERPRINT_PATTERN)
+ record.errors.add(attribute, "must be a hash containing only letters, numbers, spaces, : and -")
+ end
+ end
+end
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
index 3ec1594e202..612d3c71913 100644
--- a/app/validators/importable_url_validator.rb
+++ b/app/validators/importable_url_validator.rb
@@ -4,8 +4,8 @@
# protect against Server-side Request Forgery (SSRF).
class ImportableUrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, 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
+ Gitlab::UrlBlocker.validate!(value, valid_ports: Project::VALID_IMPORT_PORTS)
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ record.errors.add(attribute, "is blocked: #{e.message}")
end
end
diff --git a/app/validators/top_level_group_validator.rb b/app/validators/top_level_group_validator.rb
new file mode 100644
index 00000000000..7e2e735e0cf
--- /dev/null
+++ b/app/validators/top_level_group_validator.rb
@@ -0,0 +1,7 @@
+class TopLevelGroupValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if value&.subgroup?
+ record.errors.add(attribute, "must be a top level Group")
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml
new file mode 100644
index 00000000000..8198a822a10
--- /dev/null
+++ b/app/views/admin/application_settings/_background_jobs.html.haml
@@ -0,0 +1,30 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ %p
+ These settings require a
+ = link_to 'restart', help_page_path('administration/restart_gitlab')
+ to take effect.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :sidekiq_throttling_enabled do
+ = f.check_box :sidekiq_throttling_enabled
+ Enable Sidekiq Job Throttling
+ .help-block
+ Limit the amount of resources slow running jobs are assigned.
+ .form-group
+ = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
+ .help-block
+ Choose which queues you wish to throttle.
+ .form-group
+ = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
+ .help-block
+ The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
+
+ = 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 636535fba84..309c7ed5dfa 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -10,111 +10,6 @@
= f.number_field :container_registry_token_expire_delay, class: 'form-control'
%fieldset
- %legend Profiling - Performance Bar
- %p
- Enable the Performance Bar for a given group.
- = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :performance_bar_enabled do
- = f.check_box :performance_bar_enabled
- Enable the Performance Bar
- .form-group
- = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
-
- %fieldset
- %legend Background Jobs
- %p
- These settings require a
- = link_to 'restart', help_page_path('administration/restart_gitlab')
- to take effect.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :sidekiq_throttling_enabled do
- = f.check_box :sidekiq_throttling_enabled
- Enable Sidekiq Job Throttling
- .help-block
- Limit the amount of resources slow running jobs are assigned.
- .form-group
- = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
- .col-sm-10
- = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
- .help-block
- Choose which queues you wish to throttle.
- .form-group
- = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
- .help-block
- The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
-
- %fieldset
- %legend Spam and Anti-bot Protection
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :recaptcha_enabled do
- = f.check_box :recaptcha_enabled
- Enable reCAPTCHA
- %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
-
- .form-group
- = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :recaptcha_site_key, class: 'form-control'
- .help-block
- Generate site and private keys at
- %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
-
- .form-group
- = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :recaptcha_private_key, class: 'form-control'
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :akismet_enabled do
- = f.check_box :akismet_enabled
- Enable Akismet
- %span.help-block#akismet_help_block Helps prevent bots from creating issues
-
- .form-group
- = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :akismet_api_key, class: 'form-control'
- .help-block
- Generate API key at
- %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :unique_ips_limit_enabled do
- = f.check_box :unique_ips_limit_enabled
- Limit sign in from multiple ips
- %span.help-block#unique_ip_help_block
- Helps prevent malicious users hide their activity
-
- .form-group
- = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :unique_ips_limit_per_user, class: 'form-control'
- .help-block
- Maximum number of unique IPs per user
-
- .form-group
- = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :unique_ips_limit_time_window, class: 'form-control'
- .help-block
- How many seconds an IP will be counted towards the limit
-
- %fieldset
%legend Abuse reports
.form-group
= f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
new file mode 100644
index 00000000000..5344f030c97
--- /dev/null
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -0,0 +1,16 @@
+= 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 :performance_bar_enabled do
+ = f.check_box :performance_bar_enabled
+ Enable the Performance Bar
+ .form-group
+ = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
new file mode 100644
index 00000000000..25e89097dfe
--- /dev/null
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -0,0 +1,65 @@
+= 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 :recaptcha_enabled do
+ = f.check_box :recaptcha_enabled
+ Enable reCAPTCHA
+ %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
+
+ .form-group
+ = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :recaptcha_site_key, class: 'form-control'
+ .help-block
+ Generate site and private keys at
+ %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
+
+ .form-group
+ = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :recaptcha_private_key, class: 'form-control'
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :akismet_enabled do
+ = f.check_box :akismet_enabled
+ Enable Akismet
+ %span.help-block#akismet_help_block Helps prevent bots from creating issues
+
+ .form-group
+ = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :akismet_api_key, class: 'form-control'
+ .help-block
+ Generate API key at
+ %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :unique_ips_limit_enabled do
+ = f.check_box :unique_ips_limit_enabled
+ Limit sign in from multiple ips
+ %span.help-block#unique_ip_help_block
+ Helps prevent malicious users hide their activity
+
+ .form-group
+ = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :unique_ips_limit_per_user, class: 'form-control'
+ .help-block
+ Maximum number of unique IPs per user
+
+ .form-group
+ = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :unique_ips_limit_time_window, class: 'form-control'
+ .help-block
+ How many seconds an IP will be counted towards the limit
+
+ = 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 17f2f37d24e..d0e612e62e5 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -7,7 +7,7 @@
.settings-header
%h4
= _('Visibility and access controls')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
@@ -18,7 +18,7 @@
.settings-header
%h4
= _('Account and limit settings')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Session expiration, projects limit and attachment size.')
@@ -29,7 +29,7 @@
.settings-header
%h4
= _('Sign-up restrictions')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Configure the way a user creates a new account.')
@@ -40,7 +40,7 @@
.settings-header
%h4
= _('Sign-in restrictions')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
@@ -51,7 +51,7 @@
.settings-header
%h4
= _('Help page')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Help page text and support page url.')
@@ -62,7 +62,7 @@
.settings-header
%h4
= _('Pages')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Size and domain settings for static websites')
@@ -73,7 +73,7 @@
.settings-header
%h4
= _('Continuous Integration and Deployment')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Auto DevOps, runners amd job artifacts')
@@ -84,7 +84,7 @@
.settings-header
%h4
= _('Metrics - Influx')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Enable and configure InfluxDB metrics.')
@@ -95,12 +95,46 @@
.settings-header
%h4
= _('Metrics - Prometheus')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Enable and configure Prometheus metrics.')
.settings-content
= render 'prometheus'
+%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Profiling - Performance bar')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Enable the Performance Bar for a given group.')
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
+ .settings-content
+ = render 'performance_bar'
+
+%section.settings.as-background.no-animate#js-background-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Background jobs')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure Sidekiq job throttling.')
+ .settings-content
+ = render 'background_jobs'
+
+%section.settings.as-spam.no-animate#js-spam-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Spam and Anti-bot Protection')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Enable reCAPTCHA or Akismet and set IP limits.')
+ .settings-content
+ = render 'spam'
+
.prepend-top-20
= render 'form'
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 7f9486d08d9..8e1dea4afc1 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
- can_create_subgroups = can?(current_user, :create_subgroup, @group)
= content_for :meta_tags do
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 257f7326409..6513b719199 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,5 +1,5 @@
!!! 5
-%html.devise-layout-html
+%html.devise-layout-html{ class: system_message_class }
= render "layouts/head"
%body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } }
.page-wrap
@@ -16,7 +16,7 @@
%h1
= brand_title
= brand_image
- - if brand_item&.description?
+ - if current_appearance&.description?
= brand_text
- else
%h3 Open source software to collaborate on code
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index 8718bb3db1a..adf90cb7667 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,5 +1,5 @@
!!! 5
-%html{ lang: "en" }
+%html{ lang: "en", class: system_message_class }
= render "layouts/head"
%body.ui_indigo.login-page.application.navless
= render "layouts/header/empty"
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 5dfe973f33c..825bfd0707f 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -7,7 +7,7 @@
.settings-header
%h4
Export project
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index 2ee0eafcf1a..4c510293204 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -31,7 +31,7 @@
%section.settings#js-cluster-details{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('ClusterIntegration|Kubernetes cluster details')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content
@@ -43,7 +43,7 @@
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Advanced settings')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
.settings-content
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 75dd4c9ae15..7dd8dc28e5b 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -3,7 +3,7 @@
.settings-header
%h4
Deploy Keys
- %button.btn.js-settings-toggle.qa-expand-deploy-keys
+ %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index a96485ab155..99eeb9551e3 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -8,7 +8,7 @@
.settings-header
%h4
General project settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Update your project name, description, avatar, and other general settings.
@@ -64,7 +64,7 @@
.settings-header
%h4
Permissions
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Enable or disable certain project features and choose access levels.
@@ -79,7 +79,7 @@
.settings-header
%h4
Merge request settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Customize your merge request restrictions.
@@ -94,7 +94,7 @@
.settings-header
%h4
Advanced settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 8a36fada389..b15fe514a08 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
= render partial: 'flash_messages', locals: { project: @project }
diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml
index ba5845877e5..14d880028c7 100644
--- a/app/views/projects/no_repo.html.haml
+++ b/app/views/projects/no_repo.html.haml
@@ -1,3 +1,5 @@
+- breadcrumb_title _("Details")
+
%h2
%i.fa.fa-warning
#{ _('No repository') }
@@ -10,7 +12,7 @@
.no-repo-actions
= link_to project_repository_path(@project), method: :post, class: 'btn btn-primary' do
- #{ _('Create empty bare repository') }
+ #{ _('Create empty repository') }
%strong.prepend-left-10.append-right-10 or
@@ -19,4 +21,4 @@
- if can? current_user, :remove_project, @project
.prepend-top-20
- = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right"
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index e662b877fbb..55d87c35a80 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -4,7 +4,7 @@
.settings-header
%h4
Protected Branches
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index 24baf1cfc89..c33723d8072 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -4,7 +4,7 @@
.settings-header
%h4
Protected Tags
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Limit access to creating and updating tags.
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 756f31f91d9..d65341dbd40 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -8,7 +8,7 @@
.settings-header
%h4
General pipelines settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Update your CI/CD configuration, like job timeout or Auto DevOps.
@@ -19,7 +19,7 @@
.settings-header
%h4
Runners settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Register and see your runners for this project.
@@ -31,7 +31,7 @@
%h4
= _('Secret variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p.append-bottom-0
= render "ci/variables/content"
@@ -42,7 +42,7 @@
.settings-header
%h4
Pipeline triggers
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index fa281327eb7..94331a16abd 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
- show_auto_devops_callout = show_auto_devops_callout?(@project)
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index adaddda13eb..6afcd447f28 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -84,7 +84,7 @@
= dropdown_content do
.js-due-date-calendar
- - if @labels && @labels.any?
+ - if @labels
- selected_labels = issuable.labels
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 6006ab8b43f..f302299eb24 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -1,4 +1,5 @@
-- page_title milestone.title, "Milestones"
+- page_title @milestone.title
+- @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- group = local_assigns[:group]
@@ -17,7 +18,7 @@
Milestone #{milestone.title}
- if milestone.due_date || milestone.start_date
%span.creator
- &middot;
+ &nbsp;&middot;
= milestone_date_range(milestone)
- if group
.pull-right
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index 01ed123e6c8..a6b2c251254 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -138,21 +138,18 @@ module ObjectStorage
include Report
- def self.enqueue!(uploads, mounted_as, to_store)
- sanity_check!(uploads, mounted_as)
+ def self.enqueue!(uploads, model_class, mounted_as, to_store)
+ sanity_check!(uploads, model_class, mounted_as)
- perform_async(uploads.ids, mounted_as, to_store)
+ perform_async(uploads.ids, model_class.to_s, 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)
+ def self.sanity_check!(uploads, model_class, 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
@@ -162,7 +159,12 @@ module ObjectStorage
raise(SanityCheckError, "Mount point #{mounted_as} not found in #{model_class}.") unless model_has_mount
end
- def perform(ids, mounted_as, to_store)
+ def perform(*args)
+ args_check!(args)
+
+ (ids, model_type, mounted_as, to_store) = args
+
+ @model_class = model_type.constantize
@mounted_as = mounted_as&.to_sym
@to_store = to_store
@@ -178,7 +180,17 @@ module ObjectStorage
end
def sanity_check!(uploads)
- self.class.sanity_check!(uploads, @mounted_as)
+ self.class.sanity_check!(uploads, @model_class, @mounted_as)
+ end
+
+ def args_check!(args)
+ return if args.count == 4
+
+ case args.count
+ when 3 then raise SanityCheckError, "Job is missing the `model_type` argument."
+ else
+ raise SanityCheckError, "Job has wrong arguments format."
+ end
end
def build_uploaders(uploads)
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index 0b502143e5d..c3d84bb0b93 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -4,11 +4,19 @@ class ProjectExportWorker
sidekiq_options retry: 3
- def perform(current_user_id, project_id, params = {})
- params = params.with_indifferent_access
+ def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
current_user = User.find(current_user_id)
project = Project.find(project_id)
+ after_export = build!(after_export_strategy)
- ::Projects::ImportExport::ExportService.new(project, current_user, params).execute
+ ::Projects::ImportExport::ExportService.new(project, current_user, params).execute(after_export)
+ end
+
+ private
+
+ def build!(after_export_strategy)
+ strategy_klass = after_export_strategy&.delete('klass')
+
+ Gitlab::ImportExport::AfterExportStrategyBuilder.build!(strategy_klass, after_export_strategy)
end
end