summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/merge_request_templates/Documentation.md27
-rw-r--r--CHANGELOG.md20
-rw-r--r--Gemfile8
-rw-r--r--Gemfile.lock15
-rw-r--r--app/assets/javascripts/gl_dropdown.js2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue291
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue159
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue413
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue48
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue226
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue228
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue60
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue24
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js (renamed from app/assets/javascripts/pages/ci/lints/ci_lint_editor.js)0
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/new/index.js (renamed from app/assets/javascripts/pages/ci/lints/new/index.js)0
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/show/index.js (renamed from app/assets/javascripts/pages/ci/lints/show/index.js)0
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue4
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss109
-rw-r--r--app/assets/stylesheets/pages/lint.scss21
-rw-r--r--app/assets/stylesheets/pages/projects.scss22
-rw-r--r--app/assets/stylesheets/pages/repo.scss18
-rw-r--r--app/controllers/ci/lints_controller.rb15
-rw-r--r--app/controllers/projects/ci/lints_controller.rb27
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb52
-rw-r--r--app/controllers/projects/protected_branches_controller.rb8
-rw-r--r--app/controllers/projects/protected_refs_controller.rb14
-rw-r--r--app/controllers/projects/protected_tags_controller.rb8
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb6
-rw-r--r--app/controllers/root_controller.rb4
-rw-r--r--app/helpers/preferences_helper.rb14
-rw-r--r--app/models/lfs_object.rb6
-rw-r--r--app/models/merge_request.rb27
-rw-r--r--app/models/user.rb2
-rw-r--r--app/policies/protected_branch_policy.rb9
-rw-r--r--app/services/protected_branches/create_service.rb17
-rw-r--r--app/services/protected_branches/destroy_service.rb9
-rw-r--r--app/services/protected_branches/update_service.rb2
-rw-r--r--app/services/protected_tags/destroy_service.rb7
-rw-r--r--app/uploaders/object_storage.rb94
-rw-r--r--app/views/ci/lints/show.html.haml36
-rw-r--r--app/views/projects/ci/lints/_create.html.haml (renamed from app/views/ci/lints/_create.html.haml)0
-rw-r--r--app/views/projects/ci/lints/show.html.haml27
-rw-r--r--app/views/projects/pipelines/index.html.haml2
-rw-r--r--changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml5
-rw-r--r--changelogs/unreleased/43603-ci-lint-support.yml5
-rw-r--r--changelogs/unreleased/44232-docs-for-runner-ip-address.yml5
-rw-r--r--changelogs/unreleased/44564-error-500-while-attempting-to-resolve-conflicts-due-to-utf-8-conversion-error.yml5
-rw-r--r--changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml5
-rw-r--r--changelogs/unreleased/ab-44446-add-indexes-for-user-activity-queries.yml5
-rw-r--r--changelogs/unreleased/ab-44467-remove-index.yml5
-rw-r--r--changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml5
-rw-r--r--changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml5
-rw-r--r--changelogs/unreleased/dm-refs-contains-sha-encoding.yml5
-rw-r--r--changelogs/unreleased/fix-ci-job-auto-retry.yml5
-rw-r--r--changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml5
-rw-r--r--changelogs/unreleased/sh-update-loofah.yml5
-rw-r--r--changelogs/unreleased/update-unresolved-discussions-vue-component.yml5
-rw-r--r--changelogs/unreleased/workhorse-gitaly-mandatory.yml5
-rw-r--r--config/gitlab.yml.example6
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/initializers/carrierwave.rb12
-rw-r--r--config/routes/ci.rb2
-rw-r--r--config/routes/project.rb4
-rw-r--r--db/migrate/20180327101207_remove_index_from_events_table.rb18
-rw-r--r--db/schema.rb3
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/administration/job_traces.md42
-rw-r--r--doc/ci/docker/using_docker_build.md91
-rw-r--r--doc/ci/examples/README.md4
-rw-r--r--doc/ci/quick_start/README.md4
-rw-r--r--doc/ci/yaml/README.md5
-rw-r--r--doc/topics/autodevops/index.md18
-rw-r--r--doc/user/profile/preferences.md4
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md15
-rw-r--r--doc/user/project/merge_requests/img/remove_source_branch_status.pngbin0 -> 32649 bytes
-rw-r--r--doc/user/project/merge_requests/index.md16
-rw-r--r--doc/workflow/lfs/lfs_administration.md1
-rw-r--r--lib/api/protected_branches.rb5
-rw-r--r--lib/gitlab/ci/config.rb3
-rw-r--r--lib/gitlab/ci/yaml_processor.rb8
-rw-r--r--lib/gitlab/git/repository.rb3
-rw-r--r--lib/gitlab/workhorse.rb16
-rw-r--r--package.json3
-rw-r--r--spec/controllers/projects/ci/lints_controller_spec.rb123
-rw-r--r--spec/controllers/projects/protected_branches_controller_spec.rb97
-rw-r--r--spec/controllers/root_controller_spec.rb24
-rw-r--r--spec/features/projects/ci/lint_spec.rb (renamed from spec/features/ci_lint_spec.rb)8
-rw-r--r--spec/helpers/preferences_helper_spec.rb4
-rw-r--r--spec/javascripts/fixtures/gl_dropdown.html.haml3
-rw-r--r--spec/javascripts/gl_dropdown_spec.js25
-rw-r--r--spec/javascripts/helpers/vue_component_helper.js3
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js6
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js5
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js28
-rw-r--r--spec/lib/backup/manager_spec.rb4
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb2
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb5
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb8
-rw-r--r--spec/models/merge_request_spec.rb11
-rw-r--r--spec/policies/protected_branch_policy_spec.rb22
-rw-r--r--spec/requests/api/protected_branches_spec.rb34
-rw-r--r--spec/requests/lfs_http_spec.rb196
-rw-r--r--spec/services/protected_branches/create_service_spec.rb13
-rw-r--r--spec/services/protected_branches/destroy_service_spec.rb30
-rw-r--r--spec/services/protected_branches/update_service_spec.rb11
-rw-r--r--spec/services/protected_tags/destroy_service_spec.rb17
-rw-r--r--spec/support/stub_object_storage.rb13
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb4
-rw-r--r--spec/uploaders/object_storage_spec.rb352
-rw-r--r--spec/views/projects/ci/lints/show.html.haml_spec.rb (renamed from spec/views/ci/lints/show.html.haml_spec.rb)33
-rw-r--r--yarn.lock2
111 files changed, 2394 insertions, 1130 deletions
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
index 102eb7e7953..da38a703c3c 100644
--- a/.gitlab/merge_request_templates/Documentation.md
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -1,16 +1,29 @@
-See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html
+<!--See the general Documentation guidelines https://docs.gitlab.com/ce/development/writing_documentation.html -->
## What does this MR do?
-(briefly describe what this MR is about)
+<!-- Briefly describe what this MR is about -->
+
+## Related issues
+
+<!-- Mention the issue(s) this MR closes or is related to -->
+
+Closes
## Moving docs to a new location?
-See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location
+Read the guidelines:
+https://docs.gitlab.com/ce/development/writing_documentation.html#changing-document-location
-- [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location.
+- [ ] Make sure the old link is not removed and has its contents replaced with
+ a link to the new location.
- [ ] Make sure internal links pointing to the document in question are not broken.
-- [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory.
-- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/doc_styleguide.html#redirections-for-pages-with-disqus-comments) to the new document if there are any Disqus comments on the old document thread.
-- [ ] If working on CE, submit an MR to EE with the changes as well.
+- [ ] Search and replace any links referring to old docs in GitLab Rails app,
+ specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
+- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments)
+ to the new document if there are any Disqus comments on the old document thread.
+- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE
+ with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee).
- [ ] Ping one of the technical writers for review.
+
+/label ~Documentation
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4426cd20732..adb0ec9f5b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.6.1 (2018-03-27)
+
+### Security (1 change)
+
+- Bump rails-html-sanitizer to 1.0.4.
+
+### Fixed (2 changes)
+
+- Prevent auto-retry AccessDenied error from stopping transition to failed. !17862
+- Fix 500 error when trying to resolve non-ASCII conflicts in the editor. !17962
+
+### Performance (1 change)
+
+- Add indexes for user activity queries. !17890
+
+### Other (1 change)
+
+- Add documentation for runner IP address (#44232). !17837
+
+
## 10.6.0 (2018-03-22)
### Security (4 changes)
diff --git a/Gemfile b/Gemfile
index 37530641796..035c86efff3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -118,9 +118,9 @@ gem 'carrierwave', '~> 1.2'
gem 'dropzonejs-rails', '~> 0.7.1'
# for backups
-gem 'fog-aws', '~> 2.0'
+gem 'fog-aws', '~> 2.0.1'
gem 'fog-core', '~> 1.44'
-gem 'fog-google', '~> 0.5'
+gem 'fog-google', '~> 1.3.3'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
@@ -146,8 +146,8 @@ gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
-gem 'asciidoctor', '~> 1.5.2'
-gem 'asciidoctor-plantuml', '0.0.7'
+gem 'asciidoctor', '~> 1.5.6'
+gem 'asciidoctor-plantuml', '0.0.8'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index fcffe45acbf..7d8b22359b2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -56,8 +56,8 @@ GEM
faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
- asciidoctor (1.5.3)
- asciidoctor-plantuml (0.0.7)
+ asciidoctor (1.5.6.2)
+ asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
asset_sync (2.2.0)
activemodel (>= 4.1.0)
@@ -244,10 +244,11 @@ GEM
builder
excon (~> 0.58)
formatador (~> 0.2)
- fog-google (0.5.3)
+ fog-google (1.3.3)
fog-core
fog-json
fog-xml
+ google-api-client (~> 0.19.1)
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
@@ -998,8 +999,8 @@ DEPENDENCIES
akismet (~> 2.0)
allocations (~> 1.0)
asana (~> 0.6.0)
- asciidoctor (~> 1.5.2)
- asciidoctor-plantuml (= 0.0.7)
+ asciidoctor (~> 1.5.6)
+ asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0)
@@ -1047,9 +1048,9 @@ DEPENDENCIES
flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0)
fog-aliyun (~> 0.2.0)
- fog-aws (~> 2.0)
+ fog-aws (~> 2.0.1)
fog-core (~> 1.44)
- fog-google (~> 0.5)
+ fog-google (~> 1.3.3)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1)
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 86b34a6e360..fa48d7d1915 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -753,7 +753,7 @@ GitLabDropdown = (function() {
}
if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
- return;
+ return [selectedObject];
}
if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 10b3a4d2fee..f5572be5fbf 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,162 +1,155 @@
<script>
- import _ from 'underscore';
- import Flash from '../../flash';
- import MonitoringService from '../services/monitoring_service';
- import GraphGroup from './graph_group.vue';
- import Graph from './graph.vue';
- import EmptyState from './empty_state.vue';
- import MonitoringStore from '../stores/monitoring_store';
- import eventHub from '../event_hub';
+import _ from 'underscore';
+import Flash from '../../flash';
+import MonitoringService from '../services/monitoring_service';
+import GraphGroup from './graph_group.vue';
+import Graph from './graph.vue';
+import EmptyState from './empty_state.vue';
+import MonitoringStore from '../stores/monitoring_store';
+import eventHub from '../event_hub';
- export default {
- components: {
- Graph,
- GraphGroup,
- EmptyState,
+export default {
+ components: {
+ Graph,
+ GraphGroup,
+ EmptyState,
+ },
+ props: {
+ hasMetrics: {
+ type: Boolean,
+ required: false,
+ default: true,
},
-
- props: {
- hasMetrics: {
- type: Boolean,
- required: false,
- default: true,
- },
- showLegend: {
- type: Boolean,
- required: false,
- default: true,
- },
- showPanels: {
- type: Boolean,
- required: false,
- default: true,
- },
- forceSmallGraph: {
- type: Boolean,
- required: false,
- default: false,
- },
- documentationPath: {
- type: String,
- required: true,
- },
- settingsPath: {
- type: String,
- required: true,
- },
- clustersPath: {
- type: String,
- required: true,
- },
- tagsPath: {
- type: String,
- required: true,
- },
- projectPath: {
- type: String,
- required: true,
- },
- metricsEndpoint: {
- type: String,
- required: true,
- },
- deploymentEndpoint: {
- type: String,
- required: false,
- default: null,
- },
- emptyGettingStartedSvgPath: {
- type: String,
- required: true,
- },
- emptyLoadingSvgPath: {
- type: String,
- required: true,
- },
- emptyNoDataSvgPath: {
- type: String,
- required: true,
- },
- emptyUnableToConnectSvgPath: {
- type: String,
- required: true,
- },
+ showLegend: {
+ type: Boolean,
+ required: false,
+ default: true,
},
-
- data() {
- return {
- store: new MonitoringStore(),
- state: 'gettingStarted',
- showEmptyState: true,
- updateAspectRatio: false,
- updatedAspectRatios: 0,
- hoverData: {},
- resizeThrottled: {},
- };
+ showPanels: {
+ type: Boolean,
+ required: false,
+ default: true,
},
-
- created() {
- this.service = new MonitoringService({
- metricsEndpoint: this.metricsEndpoint,
- deploymentEndpoint: this.deploymentEndpoint,
- });
- eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
- eventHub.$on('hoverChanged', this.hoverChanged);
+ forceSmallGraph: {
+ type: Boolean,
+ required: false,
+ default: false,
},
-
- beforeDestroy() {
- eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
- eventHub.$off('hoverChanged', this.hoverChanged);
- window.removeEventListener('resize', this.resizeThrottled, false);
+ documentationPath: {
+ type: String,
+ required: true,
},
-
- mounted() {
- this.resizeThrottled = _.throttle(this.resize, 600);
- if (!this.hasMetrics) {
- this.state = 'gettingStarted';
- } else {
- this.getGraphsData();
- window.addEventListener('resize', this.resizeThrottled, false);
+ settingsPath: {
+ type: String,
+ required: true,
+ },
+ clustersPath: {
+ type: String,
+ required: true,
+ },
+ tagsPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ metricsEndpoint: {
+ type: String,
+ required: true,
+ },
+ deploymentEndpoint: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ emptyGettingStartedSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyLoadingSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyNoDataSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyUnableToConnectSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ store: new MonitoringStore(),
+ state: 'gettingStarted',
+ showEmptyState: true,
+ updateAspectRatio: false,
+ updatedAspectRatios: 0,
+ hoverData: {},
+ resizeThrottled: {},
+ };
+ },
+ created() {
+ this.service = new MonitoringService({
+ metricsEndpoint: this.metricsEndpoint,
+ deploymentEndpoint: this.deploymentEndpoint,
+ });
+ eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
+ eventHub.$on('hoverChanged', this.hoverChanged);
+ },
+ beforeDestroy() {
+ eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
+ eventHub.$off('hoverChanged', this.hoverChanged);
+ window.removeEventListener('resize', this.resizeThrottled, false);
+ },
+ mounted() {
+ this.resizeThrottled = _.throttle(this.resize, 600);
+ if (!this.hasMetrics) {
+ this.state = 'gettingStarted';
+ } else {
+ this.getGraphsData();
+ window.addEventListener('resize', this.resizeThrottled, false);
+ }
+ },
+ methods: {
+ getGraphsData() {
+ this.state = 'loading';
+ Promise.all([
+ this.service.getGraphsData().then(data => this.store.storeMetrics(data)),
+ this.service
+ .getDeploymentData()
+ .then(data => this.store.storeDeploymentData(data))
+ .catch(() => new Flash('Error getting deployment information.')),
+ ])
+ .then(() => {
+ if (this.store.groups.length < 1) {
+ this.state = 'noData';
+ return;
+ }
+ this.showEmptyState = false;
+ })
+ .catch(() => {
+ this.state = 'unableToConnect';
+ });
+ },
+ resize() {
+ this.updateAspectRatio = true;
+ },
+ toggleAspectRatio() {
+ this.updatedAspectRatios = this.updatedAspectRatios += 1;
+ if (this.store.getMetricsCount() === this.updatedAspectRatios) {
+ this.updateAspectRatio = !this.updateAspectRatio;
+ this.updatedAspectRatios = 0;
}
},
-
- methods: {
- getGraphsData() {
- this.state = 'loading';
- Promise.all([
- this.service.getGraphsData()
- .then(data => this.store.storeMetrics(data)),
- this.service.getDeploymentData()
- .then(data => this.store.storeDeploymentData(data))
- .catch(() => new Flash('Error getting deployment information.')),
- ])
- .then(() => {
- if (this.store.groups.length < 1) {
- this.state = 'noData';
- return;
- }
- this.showEmptyState = false;
- })
- .catch(() => { this.state = 'unableToConnect'; });
- },
-
- resize() {
- this.updateAspectRatio = true;
- },
-
- toggleAspectRatio() {
- this.updatedAspectRatios = this.updatedAspectRatios += 1;
- if (this.store.getMetricsCount() === this.updatedAspectRatios) {
- this.updateAspectRatio = !this.updateAspectRatio;
- this.updatedAspectRatios = 0;
- }
- },
-
- hoverChanged(data) {
- this.hoverData = data;
- },
+ hoverChanged(data) {
+ this.hoverData = data;
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index fbf451fce68..c77f451c2d3 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -1,91 +1,90 @@
<script>
- export default {
- props: {
- documentationPath: {
- type: String,
- required: true,
- },
- settingsPath: {
- type: String,
- required: false,
- default: '',
- },
- clustersPath: {
- type: String,
- required: false,
- default: '',
- },
- selectedState: {
- type: String,
- required: true,
- },
- emptyGettingStartedSvgPath: {
- type: String,
- required: true,
- },
- emptyLoadingSvgPath: {
- type: String,
- required: true,
- },
- emptyNoDataSvgPath: {
- type: String,
- required: true,
- },
- emptyUnableToConnectSvgPath: {
- type: String,
- required: true,
- },
+export default {
+ props: {
+ documentationPath: {
+ type: String,
+ required: true,
+ },
+ settingsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ clustersPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ selectedState: {
+ type: String,
+ required: true,
},
- data() {
- return {
- states: {
- gettingStarted: {
- svgUrl: this.emptyGettingStartedSvgPath,
- title: 'Get started with performance monitoring',
- description: `Stay updated about the performance and health
+ emptyGettingStartedSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyLoadingSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyNoDataSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyUnableToConnectSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ states: {
+ gettingStarted: {
+ svgUrl: this.emptyGettingStartedSvgPath,
+ title: 'Get started with performance monitoring',
+ description: `Stay updated about the performance and health
of your environment by configuring Prometheus to monitor your deployments.`,
- buttonText: 'Install Prometheus on clusters',
- buttonPath: this.clustersPath,
- secondaryButtonText: 'Configure existing Prometheus',
- secondaryButtonPath: this.settingsPath,
- },
- loading: {
- svgUrl: this.emptyLoadingSvgPath,
- title: 'Waiting for performance data',
- description: `Creating graphs uses the data from the Prometheus server.
+ buttonText: 'Install Prometheus on clusters',
+ buttonPath: this.clustersPath,
+ secondaryButtonText: 'Configure existing Prometheus',
+ secondaryButtonPath: this.settingsPath,
+ },
+ loading: {
+ svgUrl: this.emptyLoadingSvgPath,
+ title: 'Waiting for performance data',
+ description: `Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`,
- buttonText: 'View documentation',
- buttonPath: this.documentationPath,
- },
- noData: {
- svgUrl: this.emptyNoDataSvgPath,
- title: 'No data found',
- description: `You are connected to the Prometheus server, but there is currently
+ buttonText: 'View documentation',
+ buttonPath: this.documentationPath,
+ },
+ noData: {
+ svgUrl: this.emptyNoDataSvgPath,
+ title: 'No data found',
+ description: `You are connected to the Prometheus server, but there is currently
no data to display.`,
- buttonText: 'Configure Prometheus',
- buttonPath: this.settingsPath,
- },
- unableToConnect: {
- svgUrl: this.emptyUnableToConnectSvgPath,
- title: 'Unable to connect to Prometheus server',
- description: 'Ensure connectivity is available from the GitLab server to the ',
- buttonText: 'View documentation',
- buttonPath: this.documentationPath,
- },
+ buttonText: 'Configure Prometheus',
+ buttonPath: this.settingsPath,
+ },
+ unableToConnect: {
+ svgUrl: this.emptyUnableToConnectSvgPath,
+ title: 'Unable to connect to Prometheus server',
+ description: 'Ensure connectivity is available from the GitLab server to the ',
+ buttonText: 'View documentation',
+ buttonPath: this.documentationPath,
},
- };
- },
- computed: {
- currentState() {
- return this.states[this.selectedState];
- },
-
- showButtonDescription() {
- if (this.selectedState === 'unableToConnect') return true;
- return false;
},
+ };
+ },
+ computed: {
+ currentState() {
+ return this.states[this.selectedState];
+ },
+ showButtonDescription() {
+ if (this.selectedState === 'unableToConnect') return true;
+ return false;
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 42615d2bb8e..04d546fafa0 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -1,236 +1,229 @@
<script>
- import { scaleLinear, scaleTime } from 'd3-scale';
- import { axisLeft, axisBottom } from 'd3-axis';
- import { max, extent } from 'd3-array';
- import { select } from 'd3-selection';
- import GraphLegend from './graph/legend.vue';
- import GraphFlag from './graph/flag.vue';
- import GraphDeployment from './graph/deployment.vue';
- import GraphPath from './graph/path.vue';
- import MonitoringMixin from '../mixins/monitoring_mixins';
- import eventHub from '../event_hub';
- import measurements from '../utils/measurements';
- import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
- import createTimeSeries from '../utils/multiple_time_series';
- import bp from '../../breakpoints';
+import { scaleLinear, scaleTime } from 'd3-scale';
+import { axisLeft, axisBottom } from 'd3-axis';
+import { max, extent } from 'd3-array';
+import { select } from 'd3-selection';
+import GraphLegend from './graph/legend.vue';
+import GraphFlag from './graph/flag.vue';
+import GraphDeployment from './graph/deployment.vue';
+import GraphPath from './graph/path.vue';
+import MonitoringMixin from '../mixins/monitoring_mixins';
+import eventHub from '../event_hub';
+import measurements from '../utils/measurements';
+import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
+import createTimeSeries from '../utils/multiple_time_series';
+import bp from '../../breakpoints';
- const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
+const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
- export default {
- components: {
- GraphLegend,
- GraphFlag,
- GraphDeployment,
- GraphPath,
+export default {
+ components: {
+ GraphLegend,
+ GraphFlag,
+ GraphDeployment,
+ GraphPath,
+ },
+ mixins: [MonitoringMixin],
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
},
-
- mixins: [MonitoringMixin],
-
- props: {
- graphData: {
- type: Object,
- required: true,
- },
- updateAspectRatio: {
- type: Boolean,
- required: true,
- },
- deploymentData: {
- type: Array,
- required: true,
- },
- hoverData: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- projectPath: {
- type: String,
- required: true,
- },
- tagsPath: {
- type: String,
- required: true,
- },
- showLegend: {
- type: Boolean,
- required: false,
- default: true,
- },
- smallGraph: {
- type: Boolean,
- required: false,
- default: false,
+ updateAspectRatio: {
+ type: Boolean,
+ required: true,
+ },
+ deploymentData: {
+ type: Array,
+ required: true,
+ },
+ hoverData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ tagsPath: {
+ type: String,
+ required: true,
+ },
+ showLegend: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ smallGraph: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ baseGraphHeight: 450,
+ baseGraphWidth: 600,
+ graphHeight: 450,
+ graphWidth: 600,
+ graphHeightOffset: 120,
+ margin: {},
+ unitOfDisplay: '',
+ yAxisLabel: '',
+ legendTitle: '',
+ reducedDeploymentData: [],
+ measurements: measurements.large,
+ currentData: {
+ time: new Date(),
+ value: 0,
},
+ currentDataIndex: 0,
+ currentXCoordinate: 0,
+ currentFlagPosition: 0,
+ showFlag: false,
+ showFlagContent: false,
+ timeSeries: [],
+ realPixelRatio: 1,
+ };
+ },
+ computed: {
+ outerViewBox() {
+ return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
},
-
- data() {
+ innerViewBox() {
+ return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
+ },
+ axisTransform() {
+ return `translate(70, ${this.graphHeight - 100})`;
+ },
+ paddingBottomRootSvg() {
return {
- baseGraphHeight: 450,
- baseGraphWidth: 600,
- graphHeight: 450,
- graphWidth: 600,
- graphHeightOffset: 120,
- margin: {},
- unitOfDisplay: '',
- yAxisLabel: '',
- legendTitle: '',
- reducedDeploymentData: [],
- measurements: measurements.large,
- currentData: {
- time: new Date(),
- value: 0,
- },
- currentDataIndex: 0,
- currentXCoordinate: 0,
- currentFlagPosition: 0,
- showFlag: false,
- showFlagContent: false,
- timeSeries: [],
- realPixelRatio: 1,
+ paddingBottom: `${Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth || 0}%`,
};
},
-
- computed: {
- outerViewBox() {
- return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
- },
-
- innerViewBox() {
- return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
- },
-
- axisTransform() {
- return `translate(70, ${this.graphHeight - 100})`;
- },
-
- paddingBottomRootSvg() {
- return {
- paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
- };
- },
-
- deploymentFlagData() {
- return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
- },
+ deploymentFlagData() {
+ return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
},
-
- watch: {
- updateAspectRatio() {
- if (this.updateAspectRatio) {
- this.graphHeight = 450;
- this.graphWidth = 600;
- this.measurements = measurements.large;
- this.draw();
- eventHub.$emit('toggleAspectRatio');
- }
- },
-
- hoverData() {
- this.positionFlag();
- },
+ },
+ watch: {
+ updateAspectRatio() {
+ if (this.updateAspectRatio) {
+ this.graphHeight = 450;
+ this.graphWidth = 600;
+ this.measurements = measurements.large;
+ this.draw();
+ eventHub.$emit('toggleAspectRatio');
+ }
},
-
- mounted() {
- this.draw();
+ hoverData() {
+ this.positionFlag();
},
+ },
+ mounted() {
+ this.draw();
+ },
+ methods: {
+ draw() {
+ const breakpointSize = bp.getBreakpointSize();
+ const query = this.graphData.queries[0];
+ this.margin = measurements.large.margin;
+ if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
+ this.graphHeight = 300;
+ this.margin = measurements.small.margin;
+ this.measurements = measurements.small;
+ }
+ this.unitOfDisplay = query.unit || '';
+ this.yAxisLabel = this.graphData.y_label || 'Values';
+ this.legendTitle = query.label || 'Average';
+ this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
+ this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
+ this.baseGraphHeight = this.graphHeight;
+ this.baseGraphWidth = this.graphWidth;
- methods: {
- draw() {
- const breakpointSize = bp.getBreakpointSize();
- const query = this.graphData.queries[0];
- this.margin = measurements.large.margin;
- if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
- this.graphHeight = 300;
- this.margin = measurements.small.margin;
- this.measurements = measurements.small;
- }
- this.unitOfDisplay = query.unit || '';
- this.yAxisLabel = this.graphData.y_label || 'Values';
- this.legendTitle = query.label || 'Average';
- this.graphWidth = this.$refs.baseSvg.clientWidth -
- this.margin.left - this.margin.right;
- this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
- this.baseGraphHeight = this.graphHeight;
- this.baseGraphWidth = this.graphWidth;
-
- // pixel offsets inside the svg and outside are not 1:1
- this.realPixelRatio = (this.$refs.baseSvg.clientWidth / this.baseGraphWidth);
-
- this.renderAxesPaths();
- this.formatDeployments();
- },
-
- handleMouseOverGraph(e) {
- let point = this.$refs.graphData.createSVGPoint();
- point.x = e.clientX;
- point.y = e.clientY;
- point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
- point.x = point.x += 7;
- const firstTimeSeries = this.timeSeries[0];
- const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
- const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
- const d0 = firstTimeSeries.values[overlayIndex - 1];
- const d1 = firstTimeSeries.values[overlayIndex];
- if (d0 === undefined || d1 === undefined) return;
- const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
- const hoveredDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
- const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
- const currentDeployXPos = this.mouseOverDeployInfo(point.x);
+ // pixel offsets inside the svg and outside are not 1:1
+ this.realPixelRatio = this.$refs.baseSvg.clientWidth / this.baseGraphWidth;
- eventHub.$emit('hoverChanged', {
- hoveredDate,
- currentDeployXPos,
- });
- },
+ this.renderAxesPaths();
+ this.formatDeployments();
+ },
+ handleMouseOverGraph(e) {
+ let point = this.$refs.graphData.createSVGPoint();
+ point.x = e.clientX;
+ point.y = e.clientY;
+ point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
+ point.x = point.x += 7;
+ const firstTimeSeries = this.timeSeries[0];
+ const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
+ const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
+ const d0 = firstTimeSeries.values[overlayIndex - 1];
+ const d1 = firstTimeSeries.values[overlayIndex];
+ if (d0 === undefined || d1 === undefined) return;
+ const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
+ const hoveredDataIndex = evalTime ? overlayIndex : overlayIndex - 1;
+ const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
+ const currentDeployXPos = this.mouseOverDeployInfo(point.x);
- renderAxesPaths() {
- this.timeSeries = createTimeSeries(
- this.graphData.queries,
- this.graphWidth,
- this.graphHeight,
- this.graphHeightOffset,
- );
+ eventHub.$emit('hoverChanged', {
+ hoveredDate,
+ currentDeployXPos,
+ });
+ },
+ renderAxesPaths() {
+ this.timeSeries = createTimeSeries(
+ this.graphData.queries,
+ this.graphWidth,
+ this.graphHeight,
+ this.graphHeightOffset,
+ );
- if (!this.showLegend) {
- this.baseGraphHeight -= 50;
- } else if (this.timeSeries.length > 3) {
- this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
- }
+ if (!this.showLegend) {
+ this.baseGraphHeight -= 50;
+ } else if (this.timeSeries.length > 3) {
+ this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
+ }
- const axisXScale = d3.scaleTime()
- .range([0, this.graphWidth - 70]);
- const axisYScale = d3.scaleLinear()
- .range([this.graphHeight - this.graphHeightOffset, 0]);
+ const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
+ const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]);
- const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
- axisXScale.domain(d3.extent(allValues, d => d.time));
- axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
+ const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
+ axisXScale.domain(d3.extent(allValues, d => d.time));
+ axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
- const xAxis = d3.axisBottom()
- .scale(axisXScale)
- .ticks(this.graphWidth / 120)
- .tickFormat(timeScaleFormat);
+ const xAxis = d3
+ .axisBottom()
+ .scale(axisXScale)
+ .ticks(this.graphWidth / 120)
+ .tickFormat(timeScaleFormat);
- const yAxis = d3.axisLeft()
- .scale(axisYScale)
- .ticks(measurements.yTicks);
+ const yAxis = d3
+ .axisLeft()
+ .scale(axisYScale)
+ .ticks(measurements.yTicks);
- d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
+ d3
+ .select(this.$refs.baseSvg)
+ .select('.x-axis')
+ .call(xAxis);
- const width = this.graphWidth;
- d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis)
- .selectAll('.tick')
- .each(function createTickLines(d, i) {
- if (i > 0) {
- d3.select(this).select('line')
- .attr('x2', width)
- .attr('class', 'axis-tick');
- } // Avoid adding the class to the first tick, to prevent coloring
- }); // This will select all of the ticks once they're rendered
- },
+ const width = this.graphWidth;
+ d3
+ .select(this.$refs.baseSvg)
+ .select('.y-axis')
+ .call(yAxis)
+ .selectAll('.tick')
+ .each(function createTickLines(d, i) {
+ if (i > 0) {
+ d3
+ .select(this)
+ .select('line')
+ .attr('x2', width)
+ .attr('class', 'axis-tick');
+ } // Avoid adding the class to the first tick, to prevent coloring
+ }); // This will select all of the ticks once they're rendered
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
index 98c25307b74..4012191ceb9 100644
--- a/app/assets/javascripts/monitoring/components/graph/deployment.vue
+++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue
@@ -1,32 +1,30 @@
<script>
- export default {
- props: {
- deploymentData: {
- type: Array,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
+export default {
+ props: {
+ deploymentData: {
+ type: Array,
+ required: true,
},
-
- computed: {
- calculatedHeight() {
- return this.graphHeight - this.graphHeightOffset;
- },
+ graphHeight: {
+ type: Number,
+ required: true,
},
-
- methods: {
- transformDeploymentGroup(deployment) {
- return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
- },
+ graphHeightOffset: {
+ type: Number,
+ required: true,
},
- };
+ },
+ computed: {
+ calculatedHeight() {
+ return this.graphHeight - this.graphHeightOffset;
+ },
+ },
+ methods: {
+ transformDeploymentGroup(deployment) {
+ return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
+ },
+ },
+};
</script>
<template>
<g class="deploy-info">
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index 07aa6a3e5de..906c7c51f52 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -1,127 +1,119 @@
<script>
- import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
- import { formatRelevantDigits } from '../../../lib/utils/number_utils';
- import icon from '../../../vue_shared/components/icon.vue';
+import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
+import { formatRelevantDigits } from '../../../lib/utils/number_utils';
+import icon from '../../../vue_shared/components/icon.vue';
- export default {
- components: {
- icon,
- },
- props: {
- currentXCoordinate: {
- type: Number,
- required: true,
- },
- currentData: {
- type: Object,
- required: true,
- },
- deploymentFlagData: {
- type: Object,
- required: false,
- default: null,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
- realPixelRatio: {
- type: Number,
- required: true,
- },
- showFlagContent: {
- type: Boolean,
- required: true,
- },
- timeSeries: {
- type: Array,
- required: true,
- },
- unitOfDisplay: {
- type: String,
- required: true,
- },
- currentDataIndex: {
- type: Number,
- required: true,
- },
- legendTitle: {
- type: String,
- required: true,
- },
+export default {
+ components: {
+ icon,
+ },
+ props: {
+ currentXCoordinate: {
+ type: Number,
+ required: true,
},
-
- computed: {
- formatTime() {
- return this.deploymentFlagData ?
- timeFormat(this.deploymentFlagData.time) :
- timeFormat(this.currentData.time);
- },
-
- formatDate() {
- return this.deploymentFlagData ?
- dateFormat(this.deploymentFlagData.time) :
- dateFormat(this.currentData.time);
- },
-
- cursorStyle() {
- const xCoordinate = this.deploymentFlagData ?
- this.deploymentFlagData.xPos :
- this.currentXCoordinate;
-
- const offsetTop = 20 * this.realPixelRatio;
- const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
- const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
-
- return {
- top: `${offsetTop}px`,
- left: `${offsetLeft}px`,
- height: `${height}px`,
- };
- },
-
- flagOrientation() {
- if (this.currentXCoordinate * this.realPixelRatio > 120) {
- return 'left';
- }
- return 'right';
- },
+ currentData: {
+ type: Object,
+ required: true,
},
+ deploymentFlagData: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ graphHeight: {
+ type: Number,
+ required: true,
+ },
+ graphHeightOffset: {
+ type: Number,
+ required: true,
+ },
+ realPixelRatio: {
+ type: Number,
+ required: true,
+ },
+ showFlagContent: {
+ type: Boolean,
+ required: true,
+ },
+ timeSeries: {
+ type: Array,
+ required: true,
+ },
+ unitOfDisplay: {
+ type: String,
+ required: true,
+ },
+ currentDataIndex: {
+ type: Number,
+ required: true,
+ },
+ legendTitle: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ formatTime() {
+ return this.deploymentFlagData
+ ? timeFormat(this.deploymentFlagData.time)
+ : timeFormat(this.currentData.time);
+ },
+ formatDate() {
+ return this.deploymentFlagData
+ ? dateFormat(this.deploymentFlagData.time)
+ : dateFormat(this.currentData.time);
+ },
+ cursorStyle() {
+ const xCoordinate = this.deploymentFlagData
+ ? this.deploymentFlagData.xPos
+ : this.currentXCoordinate;
- methods: {
- seriesMetricValue(series) {
- const index = this.deploymentFlagData ?
- this.deploymentFlagData.seriesIndex :
- this.currentDataIndex;
- const value = series.values[index] &&
- series.values[index].value;
- if (isNaN(value)) {
- return '-';
- }
- return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
- },
-
- seriesMetricLabel(index, series) {
- if (this.timeSeries.length < 2) {
- return this.legendTitle;
- }
- if (series.metricTag) {
- return series.metricTag;
- }
- return `series ${index + 1}`;
- },
+ const offsetTop = 20 * this.realPixelRatio;
+ const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
+ const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
- strokeDashArray(type) {
- if (type === 'dashed') return '6, 3';
- if (type === 'dotted') return '3, 3';
- return null;
- },
+ return {
+ top: `${offsetTop}px`,
+ left: `${offsetLeft}px`,
+ height: `${height}px`,
+ };
+ },
+ flagOrientation() {
+ if (this.currentXCoordinate * this.realPixelRatio > 120) {
+ return 'left';
+ }
+ return 'right';
+ },
+ },
+ methods: {
+ seriesMetricValue(series) {
+ const index = this.deploymentFlagData
+ ? this.deploymentFlagData.seriesIndex
+ : this.currentDataIndex;
+ const value = series.values[index] && series.values[index].value;
+ if (isNaN(value)) {
+ return '-';
+ }
+ return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
+ },
+ seriesMetricLabel(index, series) {
+ if (this.timeSeries.length < 2) {
+ return this.legendTitle;
+ }
+ if (series.metricTag) {
+ return series.metricTag;
+ }
+ return `series ${index + 1}`;
+ },
+ strokeDashArray(type) {
+ if (type === 'dashed') return '6, 3';
+ if (type === 'dotted') return '3, 3';
+ return null;
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index 3149397b61f..a7a058a9203 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -1,127 +1,119 @@
<script>
- import { formatRelevantDigits } from '../../../lib/utils/number_utils';
-
- export default {
- props: {
- graphWidth: {
- type: Number,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- margin: {
- type: Object,
- required: true,
- },
- measurements: {
- type: Object,
- required: true,
- },
- legendTitle: {
- type: String,
- required: true,
- },
- yAxisLabel: {
- type: String,
- required: true,
- },
- timeSeries: {
- type: Array,
- required: true,
- },
- unitOfDisplay: {
- type: String,
- required: true,
- },
- currentDataIndex: {
- type: Number,
- required: true,
- },
- showLegendGroup: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- data() {
- return {
- yLabelWidth: 0,
- yLabelHeight: 0,
- seriesXPosition: 0,
- metricUsageXPosition: 0,
- };
- },
- computed: {
- textTransform() {
- const yCoordinate = (((this.graphHeight - this.margin.top)
- + this.measurements.axisLabelLineOffset) / 2) || 0;
-
- return `translate(15, ${yCoordinate}) rotate(-90)`;
- },
-
- rectTransform() {
- const yCoordinate = (((this.graphHeight - this.margin.top)
- + this.measurements.axisLabelLineOffset) / 2)
- + (this.yLabelWidth / 2) || 0;
-
- return `translate(0, ${yCoordinate}) rotate(-90)`;
- },
-
- xPosition() {
- return (((this.graphWidth + this.measurements.axisLabelLineOffset) / 2)
- - this.margin.right) || 0;
- },
-
- yPosition() {
- return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0;
- },
+import { formatRelevantDigits } from '../../../lib/utils/number_utils';
+export default {
+ props: {
+ graphWidth: {
+ type: Number,
+ required: true,
},
- mounted() {
- this.$nextTick(() => {
- const bbox = this.$refs.ylabel.getBBox();
- this.metricUsageXPosition = 0;
- this.seriesXPosition = 0;
- if (this.$refs.legendTitleSvg != null) {
- this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
- }
- if (this.$refs.seriesTitleSvg != null) {
- this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
- }
- this.yLabelWidth = bbox.width + 10; // Added some padding
- this.yLabelHeight = bbox.height + 5;
- });
- },
- methods: {
- translateLegendGroup(index) {
- return `translate(0, ${12 * (index)})`;
- },
-
- formatMetricUsage(series) {
- const value = series.values[this.currentDataIndex] &&
- series.values[this.currentDataIndex].value;
- if (isNaN(value)) {
- return '-';
- }
- return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
- },
+ graphHeight: {
+ type: Number,
+ required: true,
+ },
+ margin: {
+ type: Object,
+ required: true,
+ },
+ measurements: {
+ type: Object,
+ required: true,
+ },
+ legendTitle: {
+ type: String,
+ required: true,
+ },
+ yAxisLabel: {
+ type: String,
+ required: true,
+ },
+ timeSeries: {
+ type: Array,
+ required: true,
+ },
+ unitOfDisplay: {
+ type: String,
+ required: true,
+ },
+ currentDataIndex: {
+ type: Number,
+ required: true,
+ },
+ showLegendGroup: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ yLabelWidth: 0,
+ yLabelHeight: 0,
+ seriesXPosition: 0,
+ metricUsageXPosition: 0,
+ };
+ },
+ computed: {
+ textTransform() {
+ const yCoordinate =
+ (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
- createSeriesString(index, series) {
- if (series.metricTag) {
- return `${series.metricTag} ${this.formatMetricUsage(series)}`;
- }
- return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
- },
+ return `translate(15, ${yCoordinate}) rotate(-90)`;
+ },
+ rectTransform() {
+ const yCoordinate =
+ (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
+ this.yLabelWidth / 2 || 0;
- strokeDashArray(type) {
- if (type === 'dashed') return '6, 3';
- if (type === 'dotted') return '3, 3';
- return null;
- },
+ return `translate(0, ${yCoordinate}) rotate(-90)`;
+ },
+ xPosition() {
+ return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
+ },
+ yPosition() {
+ return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
+ },
+ },
+ mounted() {
+ this.$nextTick(() => {
+ const bbox = this.$refs.ylabel.getBBox();
+ this.metricUsageXPosition = 0;
+ this.seriesXPosition = 0;
+ if (this.$refs.legendTitleSvg != null) {
+ this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
+ }
+ if (this.$refs.seriesTitleSvg != null) {
+ this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
+ }
+ this.yLabelWidth = bbox.width + 10; // Added some padding
+ this.yLabelHeight = bbox.height + 5;
+ });
+ },
+ methods: {
+ translateLegendGroup(index) {
+ return `translate(0, ${12 * index})`;
+ },
+ formatMetricUsage(series) {
+ const value =
+ series.values[this.currentDataIndex] && series.values[this.currentDataIndex].value;
+ if (isNaN(value)) {
+ return '-';
+ }
+ return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
+ },
+ createSeriesString(index, series) {
+ if (series.metricTag) {
+ return `${series.metricTag} ${this.formatMetricUsage(series)}`;
+ }
+ return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
+ },
+ strokeDashArray(type) {
+ if (type === 'dashed') return '6, 3';
+ if (type === 'dotted') return '3, 3';
+ return null;
},
- };
+ },
+};
</script>
<template>
<g class="axis-label-container">
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index c9721c4cb01..881560124a5 100644
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
@@ -1,36 +1,36 @@
<script>
- export default {
- props: {
- generatedLinePath: {
- type: String,
- required: true,
- },
- generatedAreaPath: {
- type: String,
- required: true,
- },
- lineStyle: {
- type: String,
- required: false,
- default: '',
- },
- lineColor: {
- type: String,
- required: true,
- },
- areaColor: {
- type: String,
- required: true,
- },
+export default {
+ props: {
+ generatedLinePath: {
+ type: String,
+ required: true,
},
- computed: {
- strokeDashArray() {
- if (this.lineStyle === 'dashed') return '3, 1';
- if (this.lineStyle === 'dotted') return '1, 1';
- return null;
- },
+ generatedAreaPath: {
+ type: String,
+ required: true,
},
- };
+ lineStyle: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lineColor: {
+ type: String,
+ required: true,
+ },
+ areaColor: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ strokeDashArray() {
+ if (this.lineStyle === 'dashed') return '3, 1';
+ if (this.lineStyle === 'dotted') return '1, 1';
+ return null;
+ },
+ },
+};
</script>
<template>
<g>
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index f71cf614552..a6dbe42a8f0 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -1,17 +1,17 @@
<script>
- export default {
- props: {
- name: {
- type: String,
- required: true,
- },
- showPanels: {
- type: Boolean,
- required: false,
- default: true,
- },
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
},
- };
+ showPanels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js
index 9ab73be80a0..9ab73be80a0 100644
--- a/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js
diff --git a/app/assets/javascripts/pages/ci/lints/new/index.js b/app/assets/javascripts/pages/projects/ci/lints/new/index.js
index 8e8a843da0b..8e8a843da0b 100644
--- a/app/assets/javascripts/pages/ci/lints/new/index.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/new/index.js
diff --git a/app/assets/javascripts/pages/ci/lints/show/index.js b/app/assets/javascripts/pages/projects/ci/lints/show/index.js
index 8e8a843da0b..8e8a843da0b 100644
--- a/app/assets/javascripts/pages/ci/lints/show/index.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/show/index.js
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
index 04100871a94..7cc07401911 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
@@ -17,8 +17,8 @@ export default {
/>
<div class="media-body space-children">
<span class="bold">
- The source branch HEAD has recently changed.
- Please reload the page and review the changes before merging.
+ {{ s__(`mrWidget|The source branch HEAD has recently changed.
+Please reload the page and review the changes before merging`) }}
</span>
</div>
</div>
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index db36e27fa74..7f3f7e67d76 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -2,7 +2,15 @@
* Styles the GitLab application with a specific color theme
*/
-@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
+@mixin gitlab-theme(
+ $color-100,
+ $color-200,
+ $color-500,
+ $color-700,
+ $color-800,
+ $color-900,
+ $color-alternate
+) {
// Header
.navbar-gitlab {
@@ -23,7 +31,7 @@
> li {
> a:hover,
> a:focus {
- background-color: rgba($color-200, .2);
+ background-color: rgba($color-200, 0.2);
}
&.active > a,
@@ -33,7 +41,7 @@
}
&.line-separator {
- border-left: 1px solid rgba($color-200, .2);
+ border-left: 1px solid rgba($color-200, 0.2);
}
}
}
@@ -56,7 +64,7 @@
&:hover,
&:focus {
@media (min-width: $screen-sm-min) {
- background-color: rgba($color-200, .2);
+ background-color: rgba($color-200, 0.2);
}
svg {
@@ -91,34 +99,34 @@
> a {
&:hover,
&:focus {
- background-color: rgba($color-200, .2);
+ background-color: rgba($color-200, 0.2);
}
}
}
.search {
form {
- background-color: rgba($color-200, .2);
+ background-color: rgba($color-200, 0.2);
&:hover {
- background-color: rgba($color-200, .3);
+ background-color: rgba($color-200, 0.3);
}
}
.location-badge {
color: $color-100;
- background-color: rgba($color-200, .1);
+ background-color: rgba($color-200, 0.1);
border-right: 1px solid $color-800;
}
.search-input::placeholder {
- color: rgba($color-200, .8);
+ color: rgba($color-200, 0.8);
}
.search-input-wrap {
.search-icon,
.clear-icon {
- fill: rgba($color-200, .8);
+ fill: rgba($color-200, 0.8);
}
}
@@ -133,7 +141,7 @@
.search-input-wrap {
.search-icon {
- fill: rgba($color-200, .8);
+ fill: rgba($color-200, 0.8);
}
}
}
@@ -144,7 +152,6 @@
color: $color-900;
}
-
// Sidebar
.nav-sidebar li.active {
box-shadow: inset 4px 0 0 $color-700;
@@ -169,28 +176,90 @@
font-weight: $gl-font-weight-bold;
}
}
-}
+ // Web IDE
+ .ide-sidebar-link {
+ color: $color-200;
+ background-color: $color-700;
+
+ &:hover,
+ &:focus {
+ background-color: $color-500;
+ }
+
+ &:active {
+ background: $color-800;
+ }
+ }
+
+ .branch-container {
+ border-left-color: $color-700;
+ }
+
+ .branch-header-title {
+ color: $color-700;
+ }
+}
body {
&.ui_indigo {
- @include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light);
+ @include gitlab-theme(
+ $indigo-100,
+ $indigo-200,
+ $indigo-500,
+ $indigo-700,
+ $indigo-800,
+ $indigo-900,
+ $white-light
+ );
}
&.ui_dark {
- @include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light);
+ @include gitlab-theme(
+ $theme-gray-100,
+ $theme-gray-200,
+ $theme-gray-500,
+ $theme-gray-700,
+ $theme-gray-800,
+ $theme-gray-900,
+ $white-light
+ );
}
&.ui_blue {
- @include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light);
+ @include gitlab-theme(
+ $theme-blue-100,
+ $theme-blue-200,
+ $theme-blue-500,
+ $theme-blue-700,
+ $theme-blue-800,
+ $theme-blue-900,
+ $white-light
+ );
}
&.ui_green {
- @include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light);
+ @include gitlab-theme(
+ $theme-green-100,
+ $theme-green-200,
+ $theme-green-500,
+ $theme-green-700,
+ $theme-green-800,
+ $theme-green-900,
+ $white-light
+ );
}
&.ui_light {
- @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
+ @include gitlab-theme(
+ $theme-gray-900,
+ $theme-gray-700,
+ $theme-gray-800,
+ $theme-gray-700,
+ $theme-gray-700,
+ $theme-gray-100,
+ $theme-gray-700
+ );
.navbar-gitlab {
background-color: $theme-gray-100;
@@ -270,5 +339,9 @@ body {
.sidebar-top-level-items > li.active .badge {
color: $theme-gray-900;
}
+
+ .ide-sidebar-link {
+ color: $white-light;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss
deleted file mode 100644
index 68b6c5ecbd4..00000000000
--- a/app/assets/stylesheets/pages/lint.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-.ci-body {
- .incorrect-syntax {
- font-size: 18px;
- color: $lint-incorrect-color;
- }
-
- .correct-syntax {
- font-size: 18px;
- color: $lint-correct-color;
- }
-}
-
-.ci-linter {
- .ci-editor {
- height: 400px;
- }
-
- .ci-template pre {
- white-space: pre-wrap;
- }
-}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 584b0579b72..9a770d77685 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -1121,3 +1121,25 @@ pre.light-well {
padding-top: $gl-padding;
padding-bottom: 37px;
}
+
+.project-ci-body {
+ .incorrect-syntax {
+ font-size: 18px;
+ color: $lint-incorrect-color;
+ }
+
+ .correct-syntax {
+ font-size: 18px;
+ color: $lint-correct-color;
+ }
+}
+
+.project-ci-linter {
+ .ci-editor {
+ height: 400px;
+ }
+
+ .ci-template pre {
+ white-space: pre-wrap;
+ }
+}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 57b995adb64..65046f6665e 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -402,7 +402,7 @@
}
.branch-container {
- border-left: 4px solid $indigo-700;
+ border-left: 4px solid;
margin-bottom: $gl-bar-padding;
}
@@ -414,7 +414,6 @@
.branch-header-title {
flex: 1;
padding: $grid-size $gl-padding;
- color: $indigo-700;
font-weight: $gl-font-weight-bold;
svg {
@@ -767,20 +766,7 @@
.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;
- }
+ font-weight: $gl-font-weight-bold;
}
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index e9bd1689a1e..738a6a5173e 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -4,20 +4,5 @@ module Ci
def show
end
-
- def create
- @content = params[:content]
- @error = Gitlab::Ci::YamlProcessor.validation_message(@content)
- @status = @error.blank?
-
- if @error.blank?
- @config_processor = Gitlab::Ci::YamlProcessor.new(@content)
- @stages = @config_processor.stages
- @builds = @config_processor.builds
- @jobs = @config_processor.jobs
- end
-
- render :show
- end
end
end
diff --git a/app/controllers/projects/ci/lints_controller.rb b/app/controllers/projects/ci/lints_controller.rb
new file mode 100644
index 00000000000..a2185572a20
--- /dev/null
+++ b/app/controllers/projects/ci/lints_controller.rb
@@ -0,0 +1,27 @@
+class Projects::Ci::LintsController < Projects::ApplicationController
+ before_action :authorize_create_pipeline!
+
+ def show
+ end
+
+ def create
+ @content = params[:content]
+ @error = Gitlab::Ci::YamlProcessor.validation_message(@content, yaml_processor_options)
+ @status = @error.blank?
+
+ if @error.blank?
+ @config_processor = Gitlab::Ci::YamlProcessor.new(@content, yaml_processor_options)
+ @stages = @config_processor.stages
+ @builds = @config_processor.builds
+ @jobs = @config_processor.jobs
+ end
+
+ render :show
+ end
+
+ private
+
+ def yaml_processor_options
+ { project: @project, sha: project.repository.commit.sha }
+ end
+end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 6b16f1ccbbb..2515e4b9a17 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -17,20 +17,23 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def upload_authorize
set_workhorse_internal_api_content_type
- render json: Gitlab::Workhorse.lfs_upload_ok(oid, size)
+
+ authorized = LfsObjectUploader.workhorse_authorize
+ authorized.merge!(LfsOid: oid, LfsSize: size)
+
+ render json: authorized
end
def upload_finalize
- unless tmp_filename
- render_lfs_forbidden
- return
- end
-
- if store_file(oid, size, tmp_filename)
+ if store_file!(oid, size)
head 200
else
render plain: 'Unprocessable entity', status: 422
end
+ rescue ActiveRecord::RecordInvalid
+ render_400
+ rescue ObjectStorage::RemoteStoreError
+ render_lfs_forbidden
end
private
@@ -51,35 +54,28 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
params[:size].to_i
end
- def tmp_filename
- name = request.headers['X-Gitlab-Lfs-Tmp']
- return if name.include?('/')
- return unless oid.present? && name.start_with?(oid)
-
- name
- end
+ def store_file!(oid, size)
+ object = LfsObject.find_by(oid: oid, size: size)
+ unless object&.file&.exists?
+ object = create_file!(oid, size)
+ end
- def store_file(oid, size, tmp_file)
- # Define tmp_file_path early because we use it in "ensure"
- tmp_file_path = File.join(LfsObjectUploader.workhorse_upload_path, tmp_file)
+ return unless object
- object = LfsObject.find_or_create_by(oid: oid, size: size)
- file_exists = object.file.exists? || move_tmp_file_to_storage(object, tmp_file_path)
- file_exists && link_to_project(object)
- ensure
- FileUtils.rm_f(tmp_file_path)
+ link_to_project!(object)
end
- def move_tmp_file_to_storage(object, path)
- object.file = File.open(path)
- object.file.store!
- object.save
+ def create_file!(oid, size)
+ LfsObject.new(oid: oid, size: size).tap do |object|
+ object.file.store_workhorse_file!(params, :file)
+ object.save!
+ end
end
- def link_to_project(object)
+ def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id)
object.projects << storage_project
- object.save
+ object.save!
end
end
end
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index d1719f12072..64954ac9a42 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -5,12 +5,8 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
@project.repository.branches
end
- def create_service_class
- ::ProtectedBranches::CreateService
- end
-
- def update_service_class
- ::ProtectedBranches::UpdateService
+ def service_namespace
+ ::ProtectedBranches
end
def load_protected_ref
diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb
index b51bdf7aa78..9e757a8d25f 100644
--- a/app/controllers/projects/protected_refs_controller.rb
+++ b/app/controllers/projects/protected_refs_controller.rb
@@ -37,7 +37,7 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
end
def destroy
- @protected_ref.destroy
+ destroy_service_class.new(@project, current_user).execute(@protected_ref)
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
@@ -47,6 +47,18 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
protected
+ def create_service_class
+ service_namespace::CreateService
+ end
+
+ def update_service_class
+ service_namespace::UpdateService
+ end
+
+ def destroy_service_class
+ service_namespace::DestroyService
+ end
+
def access_level_attributes
%i(access_level id)
end
diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb
index a5dbd7e46ae..198c938ff35 100644
--- a/app/controllers/projects/protected_tags_controller.rb
+++ b/app/controllers/projects/protected_tags_controller.rb
@@ -5,12 +5,8 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController
@project.repository.tags
end
- def create_service_class
- ::ProtectedTags::CreateService
- end
-
- def update_service_class
- ::ProtectedTags::UpdateService
+ def service_namespace
+ ::ProtectedTags
end
def load_protected_ref
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 259809f3429..96125b549b7 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -29,12 +29,12 @@ module Projects
@project_runners = @project.runners.ordered
@assignable_runners = current_user.ci_authorized_runners
.assignable_for(project).ordered.page(params[:page]).per(20)
- @shared_runners = Ci::Runner.shared.active
+ @shared_runners = ::Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all)
end
def define_secret_variables
- @variable = Ci::Variable.new(project: project)
+ @variable = ::Ci::Variable.new(project: project)
.present(current_user: current_user)
@variables = project.variables.order_key_asc
.map { |variable| variable.present(current_user: current_user) }
@@ -42,7 +42,7 @@ module Projects
def define_triggers_variables
@triggers = @project.triggers
- @trigger = Ci::Trigger.new
+ @trigger = ::Ci::Trigger.new
end
def define_badges_variables
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index 8acefd58e77..651b82f04f4 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -42,6 +42,10 @@ class RootController < Dashboard::ProjectsController
redirect_to(dashboard_groups_path)
when 'todos'
redirect_to(dashboard_todos_path)
+ when 'issues'
+ redirect_to(issues_dashboard_path(assignee_id: current_user.id))
+ when 'merge_requests'
+ redirect_to(merge_requests_dashboard_path(assignee_id: current_user.id))
end
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 373dfd457f7..fb523cb865b 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -9,12 +9,14 @@ module PreferencesHelper
# Maps `dashboard` values to more user-friendly option text
DASHBOARD_CHOICES = {
- projects: 'Your Projects (default)',
- stars: 'Starred Projects',
- project_activity: "Your Projects' Activity",
- starred_project_activity: "Starred Projects' Activity",
- groups: "Your Groups",
- todos: "Your Todos"
+ projects: _("Your Projects (default)"),
+ stars: _("Starred Projects"),
+ project_activity: _("Your Projects' Activity"),
+ starred_project_activity: _("Starred Projects' Activity"),
+ groups: _("Your Groups"),
+ todos: _("Your Todos"),
+ issues: _("Assigned Issues"),
+ merge_requests: _("Assigned Merge Requests")
}.with_indifferent_access.freeze
# Returns an Array usable by a select field for more user-friendly option text
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 64e88d5a6a2..b7de46fa202 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -11,6 +11,12 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
+ before_save :update_file_store
+
+ def update_file_store
+ self.file_store = file.object_store
+ end
+
def project_allowed_access?(project)
projects.exists?(project.lfs_storage_project.id)
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7e6d89ec9c7..91d8be5559b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -536,18 +536,25 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff(true)
end
+ def viewable_diffs
+ @viewable_diffs ||= merge_request_diffs.viewable.to_a
+ end
+
def merge_request_diff_for(diff_refs_or_sha)
- @merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
- diffs = merge_request_diffs.viewable
- h[diff_refs_or_sha] =
- if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
- diffs.find_by_diff_refs(diff_refs_or_sha)
- else
- diffs.find_by(head_commit_sha: diff_refs_or_sha)
- end
- end
+ matcher =
+ if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
+ {
+ 'start_commit_sha' => diff_refs_or_sha.start_sha,
+ 'head_commit_sha' => diff_refs_or_sha.head_sha,
+ 'base_commit_sha' => diff_refs_or_sha.base_sha
+ }
+ else
+ { 'head_commit_sha' => diff_refs_or_sha }
+ end
- @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
+ viewable_diffs.find do |diff|
+ diff.attributes.slice(*matcher.keys) == matcher
+ end
end
def version_params_for(diff_refs)
diff --git a/app/models/user.rb b/app/models/user.rb
index fa54581d220..187878f4fb5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -187,7 +187,7 @@ class User < ActiveRecord::Base
# User's Dashboard preference
# Note: When adding an option, it MUST go on the end of the array.
- enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
+ enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos, :issues, :merge_requests]
# User's Project preference
# Note: When adding an option, it MUST go on the end of the array.
diff --git a/app/policies/protected_branch_policy.rb b/app/policies/protected_branch_policy.rb
new file mode 100644
index 00000000000..1a7faa4db40
--- /dev/null
+++ b/app/policies/protected_branch_policy.rb
@@ -0,0 +1,9 @@
+class ProtectedBranchPolicy < BasePolicy
+ delegate { @subject.project }
+
+ rule { can?(:admin_project) }.policy do
+ enable :create_protected_branch
+ enable :update_protected_branch
+ enable :destroy_protected_branch
+ end
+end
diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb
index 6212fd69077..9d947f73af1 100644
--- a/app/services/protected_branches/create_service.rb
+++ b/app/services/protected_branches/create_service.rb
@@ -1,11 +1,20 @@
module ProtectedBranches
class CreateService < BaseService
- attr_reader :protected_branch
-
def execute(skip_authorization: false)
- raise Gitlab::Access::AccessDeniedError unless skip_authorization || can?(current_user, :admin_project, project)
+ raise Gitlab::Access::AccessDeniedError unless skip_authorization || authorized?
+
+ protected_branch.save
+ protected_branch
+ end
+
+ def authorized?
+ can?(current_user, :create_protected_branch, protected_branch)
+ end
+
+ private
- project.protected_branches.create(params)
+ def protected_branch
+ @protected_branch ||= project.protected_branches.new(params)
end
end
end
diff --git a/app/services/protected_branches/destroy_service.rb b/app/services/protected_branches/destroy_service.rb
new file mode 100644
index 00000000000..8172c896e76
--- /dev/null
+++ b/app/services/protected_branches/destroy_service.rb
@@ -0,0 +1,9 @@
+module ProtectedBranches
+ class DestroyService < BaseService
+ def execute(protected_branch)
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_protected_branch, protected_branch)
+
+ protected_branch.destroy
+ end
+ end
+end
diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb
index 4b3337a5c9d..95e46645374 100644
--- a/app/services/protected_branches/update_service.rb
+++ b/app/services/protected_branches/update_service.rb
@@ -1,7 +1,7 @@
module ProtectedBranches
class UpdateService < BaseService
def execute(protected_branch)
- raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :update_protected_branch, protected_branch)
protected_branch.update(params)
protected_branch
diff --git a/app/services/protected_tags/destroy_service.rb b/app/services/protected_tags/destroy_service.rb
new file mode 100644
index 00000000000..c868d7ad8e6
--- /dev/null
+++ b/app/services/protected_tags/destroy_service.rb
@@ -0,0 +1,7 @@
+module ProtectedTags
+ class DestroyService < BaseService
+ def execute(protected_tag)
+ protected_tag.destroy
+ end
+ end
+end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index 7218cb0a0fc..30cc4425ae4 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -10,6 +10,9 @@ module ObjectStorage
UnknownStoreError = Class.new(StandardError)
ObjectStorageUnavailable = Class.new(StandardError)
+ DIRECT_UPLOAD_TIMEOUT = 4.hours
+ TMP_UPLOAD_PATH = 'tmp/upload'.freeze
+
module Store
LOCAL = 1
REMOTE = 2
@@ -124,6 +127,10 @@ module ObjectStorage
object_store_options.enabled
end
+ def direct_upload_enabled?
+ object_store_options.direct_upload
+ end
+
def background_upload_enabled?
object_store_options.background_upload
end
@@ -147,6 +154,45 @@ module ObjectStorage
def serialization_column(model_class, mount_point)
model_class.uploader_options.dig(mount_point, :mount_on) || mount_point
end
+
+ def workhorse_authorize
+ if options = workhorse_remote_upload_options
+ { RemoteObject: options }
+ else
+ { TempPath: workhorse_local_upload_path }
+ end
+ end
+
+ def workhorse_local_upload_path
+ File.join(self.root, TMP_UPLOAD_PATH)
+ end
+
+ def workhorse_remote_upload_options
+ return unless self.object_store_enabled?
+ return unless self.direct_upload_enabled?
+
+ id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-')
+ upload_path = File.join(TMP_UPLOAD_PATH, id)
+ connection = ::Fog::Storage.new(self.object_store_credentials)
+ expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT
+ options = { 'Content-Type' => 'application/octet-stream' }
+
+ {
+ ID: id,
+ GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at),
+ DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at),
+ StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
+ }
+ end
+ end
+
+ # allow to configure and overwrite the filename
+ def filename
+ @filename || super || file&.filename # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ def filename=(filename)
+ @filename = filename # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def file_storage?
@@ -195,10 +241,6 @@ module ObjectStorage
end
end
- def filename
- super || file&.filename
- end
-
#
# Move the file to another store
#
@@ -253,6 +295,18 @@ module ObjectStorage
}
end
+ def store_workhorse_file!(params, identifier)
+ filename = params["#{identifier}.name"]
+
+ if remote_object_id = params["#{identifier}.remote_id"]
+ store_remote_file!(remote_object_id, filename)
+ elsif local_path = params["#{identifier}.path"]
+ store_local_file!(local_path, filename)
+ else
+ raise RemoteStoreError, 'Bad file'
+ end
+ end
+
private
def schedule_background_upload?
@@ -261,6 +315,38 @@ module ObjectStorage
self.file_storage?
end
+ def store_remote_file!(remote_object_id, filename)
+ raise RemoteStoreError, 'Missing filename' unless filename
+
+ file_path = File.join(TMP_UPLOAD_PATH, remote_object_id)
+ file_path = Pathname.new(file_path).cleanpath.to_s
+ raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(TMP_UPLOAD_PATH + '/')
+
+ self.object_store = Store::REMOTE
+
+ # TODO:
+ # This should be changed to make use of `tmp/cache` mechanism
+ # instead of using custom upload directory,
+ # using tmp/cache makes this implementation way easier than it is today
+ CarrierWave::Storage::Fog::File.new(self, storage, file_path).tap do |file|
+ raise RemoteStoreError, 'Missing file' unless file.exists?
+
+ self.filename = filename
+ self.file = storage.store!(file)
+ end
+ end
+
+ def store_local_file!(local_path, filename)
+ raise RemoteStoreError, 'Missing filename' unless filename
+
+ root_path = File.realpath(self.class.workhorse_local_upload_path)
+ file_path = File.realpath(local_path)
+ raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(root_path)
+
+ self.object_store = Store::LOCAL
+ self.store!(UploadedFile.new(file_path, filename))
+ end
+
# this is a hack around CarrierWave. The #migrate method needs to be
# able to force the current file to the migrated file upon success.
def file=(file)
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index 3c0881caa06..22f149d1caa 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -1,27 +1,9 @@
-- page_title "CI Lint"
-- page_description "Validate your GitLab CI configuration file"
-- content_for :library_javascripts do
- = page_specific_javascript_tag('lib/ace.js')
-
-%h2 Check your .gitlab-ci.yml
-
-.ci-linter
- .row
- = form_tag ci_lint_path, method: :post do
- .form-group
- .col-sm-12
- .file-holder
- .js-file-title.file-title.clearfix
- Content of .gitlab-ci.yml
- #ci-editor.ci-editor= @content
- = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
- .col-sm-12
- .pull-left.prepend-top-10
- = submit_tag('Validate', class: 'btn btn-success submit-yml')
- .pull-right.prepend-top-10
- = button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml')
-
- .row.prepend-top-20
- .col-sm-12
- .results.ci-template
- = render partial: 'create' if defined?(@status)
+.row.empty-state
+ .col-xs-12
+ .svg-content
+ = image_tag 'illustrations/feature_moved.svg'
+ .col-xs-12
+ .text-content.text-center
+ %h4= _("GitLab CI Linter has been moved")
+ %p
+ = _("To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button.")
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/projects/ci/lints/_create.html.haml
index 30bf1384b22..30bf1384b22 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/projects/ci/lints/_create.html.haml
diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml
new file mode 100644
index 00000000000..6ca8152183d
--- /dev/null
+++ b/app/views/projects/ci/lints/show.html.haml
@@ -0,0 +1,27 @@
+- page_title "CI Lint"
+- page_description "Validate your GitLab CI configuration file"
+- content_for :library_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+
+%h2 Check your .gitlab-ci.yml
+
+.project-ci-linter
+ .row
+ = form_tag project_ci_lint_path(@project), method: :post do
+ .form-group
+ .col-sm-12
+ .file-holder
+ .js-file-title.file-title.clearfix
+ Content of .gitlab-ci.yml
+ #ci-editor.ci-editor= @content
+ = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
+ .col-sm-12
+ .pull-left.prepend-top-10
+ = submit_tag('Validate', class: 'btn btn-success submit-yml')
+ .pull-right.prepend-top-10
+ = button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml')
+
+ .row.prepend-top-20
+ .col-sm-12
+ .results.project-ci-template
+ = render partial: 'create' if defined?(@status)
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 3e6b3346787..c0ee81fe28d 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -10,6 +10,6 @@
"no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project),
- "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path,
+ "ci-lint-path" => can?(current_user, :create_pipeline, @project) && project_ci_lint_path(@project),
"reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) ,
"has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } }
diff --git a/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml b/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml
new file mode 100644
index 00000000000..cec06bf2dfe
--- /dev/null
+++ b/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed bug in dropdown selector when selecting the same selection again
+merge_request: 14631
+author: bitsapien
+type: fixed
diff --git a/changelogs/unreleased/43603-ci-lint-support.yml b/changelogs/unreleased/43603-ci-lint-support.yml
new file mode 100644
index 00000000000..8e4a92c0287
--- /dev/null
+++ b/changelogs/unreleased/43603-ci-lint-support.yml
@@ -0,0 +1,5 @@
+---
+title: Move ci/lint under project's namespace
+merge_request: 17729
+author:
+type: added
diff --git a/changelogs/unreleased/44232-docs-for-runner-ip-address.yml b/changelogs/unreleased/44232-docs-for-runner-ip-address.yml
deleted file mode 100644
index 82485d31b24..00000000000
--- a/changelogs/unreleased/44232-docs-for-runner-ip-address.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add documentation for runner IP address (#44232)
-merge_request: 17837
-author:
-type: other
diff --git a/changelogs/unreleased/44564-error-500-while-attempting-to-resolve-conflicts-due-to-utf-8-conversion-error.yml b/changelogs/unreleased/44564-error-500-while-attempting-to-resolve-conflicts-due-to-utf-8-conversion-error.yml
deleted file mode 100644
index 3fb96153b9c..00000000000
--- a/changelogs/unreleased/44564-error-500-while-attempting-to-resolve-conflicts-due-to-utf-8-conversion-error.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix 500 error when trying to resolve non-ASCII conflicts in the editor
-merge_request: 17962
-author:
-type: fixed
diff --git a/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml b/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml
new file mode 100644
index 00000000000..bdfed89d2ea
--- /dev/null
+++ b/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update asciidoctor-plantuml to 0.0.8
+merge_request: 18022
+author: Takuya Noguchi
+type: performance
diff --git a/changelogs/unreleased/ab-44446-add-indexes-for-user-activity-queries.yml b/changelogs/unreleased/ab-44446-add-indexes-for-user-activity-queries.yml
deleted file mode 100644
index 0f89c06fcee..00000000000
--- a/changelogs/unreleased/ab-44446-add-indexes-for-user-activity-queries.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add indexes for user activity queries.
-merge_request: 17890
-author:
-type: performance
diff --git a/changelogs/unreleased/ab-44467-remove-index.yml b/changelogs/unreleased/ab-44467-remove-index.yml
new file mode 100644
index 00000000000..fb772ce85d5
--- /dev/null
+++ b/changelogs/unreleased/ab-44467-remove-index.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unused index from events table.
+merge_request: 18014
+author:
+type: other
diff --git a/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml b/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml
new file mode 100644
index 00000000000..4db7f76e0af
--- /dev/null
+++ b/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Port direct upload of LFS artifacts from EE
+merge_request: 17752
+author:
+type: added
diff --git a/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml b/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml
new file mode 100644
index 00000000000..92a03070d78
--- /dev/null
+++ b/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml
@@ -0,0 +1,5 @@
+---
+title: Add 'Assigned Issues' and 'Assigned Merge Requests' as dashboard view choices for users
+merge_request: 17860
+author: Elias Werberich
+type: added
diff --git a/changelogs/unreleased/dm-refs-contains-sha-encoding.yml b/changelogs/unreleased/dm-refs-contains-sha-encoding.yml
new file mode 100644
index 00000000000..cdd9ead5a65
--- /dev/null
+++ b/changelogs/unreleased/dm-refs-contains-sha-encoding.yml
@@ -0,0 +1,5 @@
+---
+title: Fix listing commit branch/tags that contain special characters
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-ci-job-auto-retry.yml b/changelogs/unreleased/fix-ci-job-auto-retry.yml
deleted file mode 100644
index 442126461f0..00000000000
--- a/changelogs/unreleased/fix-ci-job-auto-retry.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent auto-retry AccessDenied error from stopping transition to failed
-merge_request: 17862
-author:
-type: fixed
diff --git a/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml b/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml
new file mode 100644
index 00000000000..1f793fe5e7c
--- /dev/null
+++ b/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce number of queries when viewing a merge request
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-update-loofah.yml b/changelogs/unreleased/sh-update-loofah.yml
deleted file mode 100644
index 6aff0f91939..00000000000
--- a/changelogs/unreleased/sh-update-loofah.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump rails-html-sanitizer to 1.0.4
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/update-unresolved-discussions-vue-component.yml b/changelogs/unreleased/update-unresolved-discussions-vue-component.yml
new file mode 100644
index 00000000000..246eaaae2bd
--- /dev/null
+++ b/changelogs/unreleased/update-unresolved-discussions-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Add i18n and update specs for ShaMismatch vue component
+merge_request: 17870
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/workhorse-gitaly-mandatory.yml b/changelogs/unreleased/workhorse-gitaly-mandatory.yml
new file mode 100644
index 00000000000..dc33aba48aa
--- /dev/null
+++ b/changelogs/unreleased/workhorse-gitaly-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Make all workhorse gitaly calls opt-out
+merge_request: 18002
+author:
+type: other
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index a23145de3e5..8db66037d61 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -711,7 +711,7 @@ test:
provider: AWS # Only AWS supported at the moment
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
- region: eu-central-1
+ region: us-east-1
artifacts:
path: tmp/tests/artifacts
enabled: true
@@ -725,7 +725,7 @@ test:
provider: AWS # Only AWS supported at the moment
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
- region: eu-central-1
+ region: us-east-1
uploads:
storage_path: tmp/tests/public
object_store:
@@ -734,7 +734,7 @@ test:
provider: AWS # Only AWS supported at the moment
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
- region: eu-central-1
+ region: us-east-1
gitlab:
host: localhost
port: 80
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 906ae8b6180..69b59b26d8c 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -350,6 +350,7 @@ Settings.lfs['storage_path'] = Settings.absolute(Settings.lfs['storage_path'] ||
Settings.lfs['object_store'] ||= Settingslogic.new({})
Settings.lfs['object_store']['enabled'] = false if Settings.lfs['object_store']['enabled'].nil?
Settings.lfs['object_store']['remote_directory'] ||= nil
+Settings.lfs['object_store']['direct_upload'] = false if Settings.lfs['object_store']['direct_upload'].nil?
Settings.lfs['object_store']['background_upload'] = true if Settings.lfs['object_store']['background_upload'].nil?
Settings.lfs['object_store']['proxy_download'] = false if Settings.lfs['object_store']['proxy_download'].nil?
# Convert upload connection settings to use string keys, to make Fog happy
diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb
index cd7df44351a..5cde6cbb0ff 100644
--- a/config/initializers/carrierwave.rb
+++ b/config/initializers/carrierwave.rb
@@ -28,16 +28,4 @@ if File.exist?(aws_file)
# when fog_public is false and provider is AWS or Google, defaults to 600
config.fog_authenticated_url_expiration = 1 << 29
end
-
- # Mocking Fog requests, based on: https://github.com/carrierwaveuploader/carrierwave/wiki/How-to%3A-Test-Fog-based-uploaders
- if Rails.env.test?
- Fog.mock!
- connection = ::Fog::Storage.new(
- aws_access_key_id: AWS_CONFIG['access_key_id'],
- aws_secret_access_key: AWS_CONFIG['secret_access_key'],
- provider: 'AWS',
- region: AWS_CONFIG['region']
- )
- connection.directories.create(key: AWS_CONFIG['bucket'])
- end
end
diff --git a/config/routes/ci.rb b/config/routes/ci.rb
index 60c1724bc05..ebd321ed097 100644
--- a/config/routes/ci.rb
+++ b/config/routes/ci.rb
@@ -1,5 +1,5 @@
namespace :ci do
- resource :lint, only: [:show, :create]
+ resource :lint, only: :show
root to: redirect('')
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index f50b9aded8d..48ba8ef06f9 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -280,6 +280,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :keep
end
end
+
+ namespace :ci do
+ resource :lint, only: [:show, :create]
+ end
end
draw :legacy_builds
diff --git a/db/migrate/20180327101207_remove_index_from_events_table.rb b/db/migrate/20180327101207_remove_index_from_events_table.rb
new file mode 100644
index 00000000000..172441da65b
--- /dev/null
+++ b/db/migrate/20180327101207_remove_index_from_events_table.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveIndexFromEventsTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index :events, :author_id
+ end
+
+ def down
+ add_concurrent_index :events, :author_id
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b3b2d5b0da9..3bf42080870 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180323150945) do
+ActiveRecord::Schema.define(version: 20180327101207) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -732,7 +732,6 @@ ActiveRecord::Schema.define(version: 20180323150945) do
add_index "events", ["action"], name: "index_events_on_action", using: :btree
add_index "events", ["author_id", "project_id"], name: "index_events_on_author_id_and_project_id", using: :btree
- add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
add_index "events", ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree
add_index "events", ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id", using: :btree
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 69efaf75140..4366590578a 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -111,6 +111,7 @@ server with IMAP authentication on Ubuntu, to be used with Reply by email.
- [Enable/disable GitLab CI/CD](../ci/enable_or_disable_ci.md#site-wide-admin-setting): Enable or disable GitLab CI/CD for your instance.
- [GitLab CI/CD admin settings](../user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration time.
- [Job artifacts](job_artifacts.md): Enable, disable, and configure job artifacts (a set of files and directories which are outputted by a job when it completes successfully).
+- [Job traces](job_traces.md): Information about the job traces (logs).
- [Artifacts size and expiration](../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size): Define maximum artifacts limits and expiration date.
- [Register Shared and specific Runners](../ci/runners/README.md#registering-a-shared-runner): Learn how to register and configure Shared and specific Runners to your own instance.
- [Shared Runners pipelines quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota): Limit the usage of pipeline minutes for Shared Runners.
diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md
new file mode 100644
index 00000000000..84a1ffeec98
--- /dev/null
+++ b/doc/administration/job_traces.md
@@ -0,0 +1,42 @@
+# Job traces (logs)
+
+By default, all job traces (logs) are saved to `/var/opt/gitlab/gitlab-ci/builds`
+and `/home/git/gitlab/builds` for Omnibus packages and installations from source
+respectively. The job logs are organized by year and month (for example, `2017_03`),
+and then by project ID.
+
+There isn't a way to automatically expire old job logs, but it's safe to remove
+them if they're taking up too much space. If you remove the logs manually, the
+job output in the UI will be empty.
+
+## Changing the job traces location
+
+To change the location where the job logs will be stored, follow the steps below.
+
+**In Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add or amend the following line:
+
+ ```
+ gitlab_ci['builds_directory'] = '/mnt/to/gitlab-ci/builds'
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+ ```yaml
+ gitlab_ci:
+ # The location where build traces are stored (default: builds/).
+ # Relative paths are relative to Rails.root.
+ builds_path: path/to/builds/
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
+[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 22afcb9199d..183808641c0 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -1,26 +1,29 @@
-# Using Docker Build
+# Building Docker images with GitLab CI/CD
-GitLab CI allows you to use Docker Engine to build and test docker-based projects.
+GitLab CI/CD allows you to use Docker Engine to build and test docker-based projects.
-**This also allows to you to use `docker-compose` and other docker-enabled tools.**
+TIP: **Tip:**
+This also allows to you to use `docker-compose` and other docker-enabled tools.
One of the new trends in Continuous Integration/Deployment is to:
-1. create an application image,
-1. run tests against the created image,
-1. push image to a remote registry, and
-1. deploy to a server from the pushed image.
+1. Create an application image
+1. Run tests against the created image
+1. Push image to a remote registry
+1. Deploy to a server from the pushed image
-It's also useful when your application already has the `Dockerfile` that can be used to create and test an image:
+It's also useful when your application already has the `Dockerfile` that can be
+used to create and test an image:
```bash
-$ docker build -t my-image dockerfiles/
-$ docker run my-docker-image /script/to/run/tests
-$ docker tag my-image my-registry:5000/my-image
-$ docker push my-registry:5000/my-image
+docker build -t my-image dockerfiles/
+docker run my-docker-image /script/to/run/tests
+docker tag my-image my-registry:5000/my-image
+docker push my-registry:5000/my-image
```
-This requires special configuration of GitLab Runner to enable `docker` support during jobs.
+This requires special configuration of GitLab Runner to enable `docker` support
+during jobs.
## Runner Configuration
@@ -74,8 +77,8 @@ GitLab Runner then executes job scripts as the `gitlab-runner` user.
5. You can now use `docker` command and install `docker-compose` if needed.
-> **Note:**
-* By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions.
+NOTE: **Note:**
+By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions.
For more information please read [On Docker security: `docker` group considered harmful](https://www.andreas-jung.com/contents/on-docker-security-docker-group-considered-harmful).
### Use docker-in-docker executor
@@ -259,8 +262,66 @@ aware of the following implications:
docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests
```
+## Making docker-in-docker builds faster with Docker layer caching
+
+When using docker-in-docker, Docker will download all layers of your image every
+time you create a build. Recent versions of Docker (Docker 1.13 and above) can
+use a pre-existing image as a cache during the `docker build` step, considerably
+speeding up the build process.
+
+### How Docker caching works
+
+When running `docker build`, each command in `Dockerfile` results in a layer.
+These layers are kept around as a cache and can be reused if there haven't been
+any changes. Change in one layer causes all subsequent layers to be recreated.
+
+You can specify a tagged image to be used as a cache source for the `docker build`
+command by using the `--cache-from` argument. Multiple images can be specified
+as a cache source by using multiple `--cache-from` arguments. Keep in mind that
+any image that's used with the `--cache-from` argument must first be pulled
+(using `docker pull`) before it can be used as a cache source.
+
+### Using Docker caching
+
+Here's a simple `.gitlab-ci.yml` file showing how Docker caching can be utilized:
+
+```yaml
+image: docker:latest
+
+services:
+ - docker:dind
+
+variables:
+ CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
+ DOCKER_DRIVER: overlay2
+
+before_script:
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
+
+build:
+ stage: build
+ script:
+ - docker pull $CONTAINER_IMAGE:latest || true
+ - docker build --cache-from $CONTAINER_IMAGE:latest --tag $CONTAINER_IMAGE:$CI_BUILD_REF --tag $CONTAINER_IMAGE:latest .
+ - docker push $CONTAINER_IMAGE:$CI_BUILD_REF
+ - docker push $CONTAINER_IMAGE:latest
+```
+
+The steps in the `script` section for the `build` stage can be summed up to:
+
+1. The first command tries to pull the image from the registry so that it can be
+ used as a cache for the `docker build` command.
+1. The second command builds a Docker image using the pulled image as a
+ cache (notice the `--cache-from $CONTAINER_IMAGE:latest` argument) if
+ available, and tags it.
+1. The last two commands push the tagged Docker images to the container registry
+ so that they may also be used as cache for subsequent builds.
+
## Using the OverlayFS driver
+NOTE: **Note:**
+The shared Runners on GitLab.com use the `overlay2` driver by default.
+
By default, when using `docker:dind`, Docker uses the `vfs` storage driver which
copies the filesystem on every run. This is a very disk-intensive operation
which can be avoided if a different driver is used, for example `overlay2`.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index c1e258aedca..de60cd27cd1 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -49,6 +49,10 @@ There's also a collection of repositories with [example projects](https://gitlab
**(Ultimate)** [Scan your code for vulnerabilities](https://docs.gitlab.com/ee/ci/examples/sast.html)
+## Dependency Scanning
+
+**(Ultimate)** [Scan your dependencies for vulnerabilities](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html)
+
## Container Scanning
[Scan your Docker images for vulnerabilities](container_scanning.md)
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index e504b81eae8..f64e868d390 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -104,8 +104,8 @@ Jobs are used to create jobs, which are then picked by
What is important is that each job is run independently from each other.
-If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
-Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
+If you want to check whether the `.gitlab-ci.yml` of your project is valid, there is a
+Lint tool under the page `/ci/lint` of your project namespace. You can also find
a "CI Lint" button to go to this page under **CI/CD âž” Pipelines** and
**Pipelines âž” Jobs** in your project.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 7184f3367be..c2b06e53c2f 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1526,8 +1526,9 @@ capitalization, the commit will be created but the pipeline will be skipped.
## Validate the .gitlab-ci.yml
-Each instance of GitLab CI has an embedded debug tool called Lint.
-You can find the link under `/ci/lint` of your gitlab instance.
+Each instance of GitLab CI has an embedded debug tool called Lint, which validates the
+content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your
+project namespace (e.g, `http://gitlab-example.com/gitlab-org/project-123/ci/lint`)
## Using reserved keywords
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 4dc3adc1441..e88b787187c 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -20,6 +20,7 @@ project in an easy and automatic way:
1. [Auto Test](#auto-test)
1. [Auto Code Quality](#auto-code-quality)
1. [Auto SAST (Static Application Security Testing)](#auto-sast)
+1. [Auto Dependency Scanning](#auto-dependency-scanning)
1. [Auto Container Scanning](#auto-container-scanning)
1. [Auto Review Apps](#auto-review-apps)
1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast)
@@ -95,7 +96,7 @@ Auto Deploy, and Auto Monitoring will be silently skipped.
The Auto DevOps base domain is required if you want to make use of [Auto
Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined
-either under the project's CI/CD settings while
+either under the project's CI/CD settings while
[enabling Auto DevOps](#enabling-auto-devops) or in instance-wide settings in
the CI/CD section.
It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`.
@@ -209,7 +210,7 @@ target branches are also
> Introduced in [GitLab Ultimate][ee] 10.3.
Static Application Security Testing (SAST) uses the
-[gl-sast Docker image](https://gitlab.com/gitlab-org/gl-sast) to run static
+[SAST Docker image](https://gitlab.com/gitlab-org/security-products/sast) to run static
analysis on the current code and checks for potential security issues. Once the
report is created, it's uploaded as an artifact which you can later download and
check out.
@@ -217,6 +218,19 @@ check out.
In GitLab Ultimate, any security warnings are also
[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html).
+### Auto Dependency Scanning
+
+> Introduced in [GitLab Ultimate][ee] 10.7.
+
+Dependency Scanning uses the
+[Dependency Scanning Docker image](https://gitlab.com/gitlab-org/security-products/dependency-scanning)
+to run analysis on the project dependencies and checks for potential security issues. Once the
+report is created, it's uploaded as an artifact which you can later download and
+check out.
+
+In GitLab Ultimate, any security warnings are also
+[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html).
+
### Auto Container Scanning
> Introduced in GitLab 10.4.
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index 022d6317555..930e506802a 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -41,7 +41,7 @@ select few, the amount of activity on the default Dashboard page can be
overwhelming. Changing this setting allows you to redefine what your default
dashboard will be.
-You have 6 options here that you can use for your default dashboard view:
+You have 8 options here that you can use for your default dashboard view:
- Your projects (default)
- Starred projects
@@ -49,6 +49,8 @@ You have 6 options here that you can use for your default dashboard view:
- Starred projects' activity
- Your groups
- Your [Todos]
+- Assigned Issues
+- Assigned Merge Requests
### Project home page content
diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md
index 8ac753c07bf..6b190deaa6c 100644
--- a/doc/user/project/integrations/prometheus_library/kubernetes.md
+++ b/doc/user/project/integrations/prometheus_library/kubernetes.md
@@ -11,10 +11,17 @@ integration services must be enabled.
## Metrics supported
-| Name | Query |
-| ---- | ----- |
-| Average Memory Usage (MB) | avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024 |
-| Average CPU Utilization (%) | avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name)) |
+- Average Memory Usage (MB):
+
+ ```
+ avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024
+ ```
+
+- Average CPU Utilization (%):
+
+ ```
+ avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))
+ ```
## Configuring Prometheus to monitor for Kubernetes metrics
diff --git a/doc/user/project/merge_requests/img/remove_source_branch_status.png b/doc/user/project/merge_requests/img/remove_source_branch_status.png
new file mode 100644
index 00000000000..1377fab54ec
--- /dev/null
+++ b/doc/user/project/merge_requests/img/remove_source_branch_status.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 10d67729734..3640d236db4 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -77,6 +77,22 @@ You can [search and filter the results](../../search/index.md#issues-and-merge-r
![Group Issues list view](img/group_merge_requests_list_view.png)
+## Removing the source branch
+
+When creating a merge request, select the "Remove source branch when merge
+request accepted" option and the source branch will be removed when the merge
+request is merged.
+
+This option is also visible in an existing merge request next to the merge
+request button and can be selected/deselected before merging. It's only visible
+to users with [Master permissions](../../permissions.md) in the source project.
+
+If the user viewing the merge request does not have the correct permissions to
+remove the source branch and the source branch is set for removal, the merge
+request widget will show the "Removes source branch" text.
+
+![Remove source branch status](img/remove_source_branch_status.png)
+
## Authorization for merge requests
There are two main ways to have a merge request flow with GitLab:
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index ca28e0a3304..cac3cb599dd 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -63,6 +63,7 @@ For source installations the following settings are nested under `lfs:` and then
|---------|-------------|---------|
| `enabled` | Enable/disable object storage | `false` |
| `remote_directory` | The bucket name where LFS objects will be stored| |
+| `direct_upload` | Set to true to enable direct upload of LFS without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. | `false` |
| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
| `connection` | Various connection options described below | |
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index 33321db46e9..aa7cab4a741 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -70,7 +70,10 @@ module API
delete ':id/protected_branches/:name', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
protected_branch = user_project.protected_branches.find_by!(name: params[:name])
- destroy_conditionally!(protected_branch)
+ destroy_conditionally!(protected_branch) do
+ destroy_service = ::ProtectedBranches::DestroyService.new(user_project, current_user)
+ destroy_service.execute(protected_branch)
+ end
end
end
end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index f7ff7ea212e..66ac4a40616 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -4,7 +4,8 @@ module Gitlab
# Base GitLab CI Configuration facade
#
class Config
- def initialize(config)
+ # EE would override this and utilize opts argument
+ def initialize(config, opts = {})
@config = Loader.new(config).load!
@global = Entry::Global.new(@config)
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index bc2a6f98dae..e829f2a95f8 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -7,8 +7,8 @@ module Gitlab
attr_reader :cache, :stages, :jobs
- def initialize(config)
- @ci_config = Gitlab::Ci::Config.new(config)
+ def initialize(config, opts = {})
+ @ci_config = Gitlab::Ci::Config.new(config, opts)
@config = @ci_config.to_hash
unless @ci_config.valid?
@@ -73,11 +73,11 @@ module Gitlab
end
end
- def self.validation_message(content)
+ def self.validation_message(content, opts = {})
return 'Please provide content of .gitlab-ci.yml' if content.blank?
begin
- Gitlab::Ci::YamlProcessor.new(content)
+ Gitlab::Ci::YamlProcessor.new(content, opts)
nil
rescue ValidationError => e
e.message
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 20b0647fce9..2d16a81c888 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -8,6 +8,7 @@ module Gitlab
class Repository
include Gitlab::Git::RepositoryMirroring
include Gitlab::Git::Popen
+ include Gitlab::EncodingHelper
ALLOWED_OBJECT_DIRECTORIES_VARIABLES = %w[
GIT_OBJECT_DIRECTORY
@@ -1479,7 +1480,7 @@ module Gitlab
names.lines.each do |line|
next unless line.start_with?(refs_prefix)
- refs << line.rstrip[left_slice_count..-1]
+ refs << encode_utf8(line.rstrip[left_slice_count..-1])
end
refs
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 0b0d667d4fd..1887f0dc2b3 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -39,20 +39,12 @@ module Gitlab
params
end
- def lfs_upload_ok(oid, size)
- {
- StoreLFSPath: LfsObjectUploader.workhorse_upload_path,
- LfsOid: oid,
- LfsSize: size
- }
- end
-
def artifact_upload_ok
{ TempPath: JobArtifactUploader.workhorse_upload_path }
end
def send_git_blob(repository, blob)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show)
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show, Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
'GitalyServer' => gitaly_server_hash(repository),
'GetBlobRequest' => {
@@ -80,7 +72,7 @@ module Gitlab
params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format)
raise "Repository or ref not found" if params.empty?
- if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive)
+ if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive, Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
params.merge!(
'GitalyServer' => gitaly_server_hash(repository),
'GitalyRepository' => repository.gitaly_repository.to_h
@@ -97,7 +89,7 @@ module Gitlab
end
def send_git_diff(repository, diff_refs)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff)
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
'GitalyServer' => gitaly_server_hash(repository),
'RawDiffRequest' => Gitaly::RawDiffRequest.new(
@@ -115,7 +107,7 @@ module Gitlab
end
def send_git_patch(repository, diff_refs)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch)
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch, Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
'GitalyServer' => gitaly_server_hash(repository),
'RawPatchRequest' => Gitaly::RawPatchRequest.new(
diff --git a/package.json b/package.json
index 56fd2575e91..31edc3a8016 100644
--- a/package.json
+++ b/package.json
@@ -121,5 +121,8 @@
"nodemon": "^1.15.1",
"prettier": "1.11.1",
"webpack-dev-server": "^2.11.2"
+ },
+ "optionalDependencies": {
+ "fsevents": "^1.1.3"
}
}
diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb
new file mode 100644
index 00000000000..1249a5528a9
--- /dev/null
+++ b/spec/controllers/projects/ci/lints_controller_spec.rb
@@ -0,0 +1,123 @@
+require 'spec_helper'
+
+describe Projects::Ci::LintsController do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #show' do
+ context 'with enough privileges' do
+ before do
+ project.add_developer(user)
+
+ get :show, namespace_id: project.namespace, project_id: project
+ end
+
+ it 'should be success' do
+ expect(response).to be_success
+ end
+
+ it 'should render show page' do
+ expect(response).to render_template :show
+ end
+
+ it 'should retrieve project' do
+ expect(assigns(:project)).to eq(project)
+ end
+ end
+
+ context 'without enough privileges' do
+ before do
+ project.add_guest(user)
+
+ get :show, namespace_id: project.namespace, project_id: project
+ end
+
+ it 'should respond with 404' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST #create' do
+ let(:remote_file_path) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
+
+ let(:remote_file_content) do
+ <<~HEREDOC
+ before_script:
+ - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
+ - ruby -v
+ - which ruby
+ - gem install bundler --no-ri --no-rdoc
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
+ HEREDOC
+ end
+
+ let(:content) do
+ <<~HEREDOC
+ include:
+ - #{remote_file_path}
+
+ rubocop:
+ script:
+ - bundle exec rubocop
+ HEREDOC
+ end
+
+ context 'with a valid gitlab-ci.yml' do
+ before do
+ WebMock.stub_request(:get, remote_file_path).to_return(body: remote_file_content)
+ project.add_developer(user)
+
+ post :create, namespace_id: project.namespace, project_id: project, content: content
+ end
+
+ it 'should be success' do
+ expect(response).to be_success
+ end
+
+ it 'render show page' do
+ expect(response).to render_template :show
+ end
+
+ it 'should retrieve project' do
+ expect(assigns(:project)).to eq(project)
+ end
+ end
+
+ context 'with an invalid gitlab-ci.yml' do
+ let(:content) do
+ <<~HEREDOC
+ rubocop:
+ scriptt:
+ - bundle exec rubocop
+ HEREDOC
+ end
+
+ before do
+ project.add_developer(user)
+
+ post :create, namespace_id: project.namespace, project_id: project, content: content
+ end
+
+ it 'should assign errors' do
+ expect(assigns[:error]).to eq('jobs:rubocop config contains unknown keys: scriptt')
+ end
+ end
+
+ context 'without enough privileges' do
+ before do
+ project.add_guest(user)
+
+ post :create, namespace_id: project.namespace, project_id: project, content: content
+ end
+
+ it 'should respond with 404' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index 80be135b5d8..096e29bc39f 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -1,6 +1,16 @@
require('spec_helper')
describe Projects::ProtectedBranchesController do
+ let(:project) { create(:project, :repository) }
+ let(:protected_branch) { create(:protected_branch, project: project) }
+ let(:project_params) { { namespace_id: project.namespace.to_param, project_id: project } }
+ let(:base_params) { project_params.merge(id: protected_branch.id) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ end
+
describe "GET #index" do
let(:project) { create(:project_empty_repo, :public) }
@@ -8,4 +18,91 @@ describe Projects::ProtectedBranchesController do
get(:index, namespace_id: project.namespace.to_param, project_id: project)
end
end
+
+ describe "POST #create" do
+ let(:master_access_level) { [{ access_level: Gitlab::Access::MASTER }] }
+ let(:access_level_params) do
+ { merge_access_levels_attributes: master_access_level,
+ push_access_levels_attributes: master_access_level }
+ end
+ let(:create_params) { attributes_for(:protected_branch).merge(access_level_params) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'creates the protected branch rule' do
+ expect do
+ post(:create, project_params.merge(protected_branch: create_params))
+ end.to change(ProtectedBranch, :count).by(1)
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ allow(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents creation of the protected branch rule" do
+ post(:create, project_params.merge(protected_branch: create_params))
+
+ expect(ProtectedBranch.count).to eq 0
+ end
+ end
+ end
+
+ describe "PUT #update" do
+ let(:update_params) { { name: 'new_name' } }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'updates the protected branch rule' do
+ put(:update, base_params.merge(protected_branch: update_params))
+
+ expect(protected_branch.reload.name).to eq('new_name')
+ expect(json_response["name"]).to eq('new_name')
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ allow(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents update of the protected branch rule" do
+ old_name = protected_branch.name
+
+ put(:update, base_params.merge(protected_branch: update_params))
+
+ expect(protected_branch.reload.name).to eq(old_name)
+ end
+ end
+ end
+
+ describe "DELETE #destroy" do
+ before do
+ sign_in(user)
+ end
+
+ it "deletes the protected branch rule" do
+ delete(:destroy, base_params)
+
+ expect { ProtectedBranch.find(protected_branch.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ allow(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ delete(:destroy, base_params)
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
end
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index b32eb39b1fb..7688538a468 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -90,6 +90,30 @@ describe RootController do
end
end
+ context 'who has customized their dashboard setting for assigned issues' do
+ before do
+ user.dashboard = 'issues'
+ end
+
+ it 'redirects to their assigned issues' do
+ get :index
+
+ expect(response).to redirect_to issues_dashboard_path(assignee_id: user.id)
+ end
+ end
+
+ context 'who has customized their dashboard setting for assigned merge requests' do
+ before do
+ user.dashboard = 'merge_requests'
+ end
+
+ it 'redirects to their assigned merge requests' do
+ get :index
+
+ expect(response).to redirect_to merge_requests_dashboard_path(assignee_id: user.id)
+ end
+ end
+
context 'who uses the default dashboard setting' do
it 'renders the default dashboard' do
get :index
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/projects/ci/lint_spec.rb
index 220b934154e..313950072e7 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/projects/ci/lint_spec.rb
@@ -1,10 +1,14 @@
require 'spec_helper'
describe 'CI Lint', :js do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
before do
- sign_in(create(:user))
+ project.add_developer(user)
+ sign_in(user)
- visit ci_lint_path
+ visit project_ci_lint_path(project)
find('#ci-editor')
execute_script("ace.edit('ci-editor').setValue(#{yaml_content.to_json});")
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index e2a0c4322ff..c9d2ec8a4ae 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -21,7 +21,9 @@ describe PreferencesHelper do
["Your Projects' Activity", 'project_activity'],
["Starred Projects' Activity", 'starred_project_activity'],
["Your Groups", 'groups'],
- ["Your Todos", 'todos']
+ ["Your Todos", 'todos'],
+ ["Assigned Issues", 'issues'],
+ ["Assigned Merge Requests", 'merge_requests']
]
end
end
diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml
index a20390c08ee..43d57c2c4dc 100644
--- a/spec/javascripts/fixtures/gl_dropdown.html.haml
+++ b/spec/javascripts/fixtures/gl_dropdown.html.haml
@@ -1,7 +1,8 @@
%div
.dropdown.inline
%button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
- Projects
+ .dropdown-toggle-text
+ Projects
%i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 0e4a7017406..5393502196e 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -256,4 +256,29 @@ describe('glDropdown', function describeDropdown() {
});
});
});
+
+ it('should keep selected item after selecting a second time', () => {
+ const options = {
+ isSelectable(item, $el) {
+ return !$el.hasClass('is-active');
+ },
+ toggleLabel(item) {
+ return item && item.id;
+ },
+ };
+ initDropDown.call(this, false, false, options);
+ const $item = $(`${ITEM_SELECTOR}:first() a`, this.$dropdownMenuElement);
+
+ // select item the first time
+ this.dropdownButtonElement.click();
+ $item.click();
+ expect($item).toHaveClass('is-active');
+ // select item the second time
+ this.dropdownButtonElement.click();
+ $item.click();
+ expect($item).toHaveClass('is-active');
+
+ expect($('.dropdown-toggle-text')).toHaveText(this.projectsData[0].id.toString());
+ });
});
+
diff --git a/spec/javascripts/helpers/vue_component_helper.js b/spec/javascripts/helpers/vue_component_helper.js
new file mode 100644
index 00000000000..257c9f5526a
--- /dev/null
+++ b/spec/javascripts/helpers/vue_component_helper.js
@@ -0,0 +1,3 @@
+export default function removeBreakLine (data) {
+ return data.replace(/\r?\n|\r/g, ' ');
+}
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index 5323523abc0..fcbd8169bc7 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import removeBreakLine from 'spec/helpers/vue_component_helper';
describe('MRWidgetConflicts', () => {
let Component;
@@ -78,8 +79,9 @@ describe('MRWidgetConflicts', () => {
});
it('should tell you to rebase locally', () => {
- expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('Fast-forward merge is not possible.');
- expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('To merge this request, first rebase locally');
+ expect(
+ removeBreakLine(vm.$el.textContent).trim(),
+ ).toContain('Fast-forward merge is not possible. To merge this request, first rebase locally.');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
index baacbc03fb1..894dbe3382f 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import removeBreakLine from 'spec/helpers/vue_component_helper';
describe('MRWidgetPipelineBlocked', () => {
let vm;
@@ -18,6 +19,8 @@ describe('MRWidgetPipelineBlocked', () => {
});
it('renders information text', () => {
- expect(vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ')).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed');
+ expect(
+ removeBreakLine(vm.$el.textContent).trim(),
+ ).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
index 25684861724..b02af94d03a 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -1,17 +1,25 @@
import Vue from 'vue';
import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import removeBreakLine from 'spec/helpers/vue_component_helper';
describe('ShaMismatch', () => {
- describe('template', () => {
+ let vm;
+
+ beforeEach(() => {
const Component = Vue.extend(ShaMismatch);
- const vm = new Component({
- el: document.createElement('div'),
- });
- it('should have correct elements', () => {
- expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed.');
- expect(vm.$el.innerText).toContain('Please reload the page and review the changes before merging.');
- });
+ vm = mountComponent(Component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render information message', () => {
+ expect(vm.$el.querySelector('button').disabled).toEqual(true);
+
+ expect(
+ removeBreakLine(vm.$el.textContent).trim(),
+ ).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging');
});
});
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index 5100f5737c2..84688845fa5 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -278,6 +278,10 @@ describe Backup::Manager do
connection.directories.create(key: Gitlab.config.backup.upload.remote_directory)
end
+ after do
+ Fog.unmock!
+ end
+
context 'target path' do
it 'uses the tar filename by default' do
expect_any_instance_of(Fog::Collection).to receive(:create)
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 2a0e19ae796..e1782cff81a 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -48,7 +48,7 @@ module Gitlab
},
'images' => {
input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
- output: "<img src=\"https://localhost.com/image.png\" alt=\"Alt text\">"
+ output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt='Alt text\" onerror=\"alert(7)'></span></p>\n</div>"
},
'pre' => {
input: '```mypre"><script>alert(3)</script>',
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 54ada3e423f..0e315b3f49e 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -604,17 +604,20 @@ describe Gitlab::Git::Repository, seed_helper: true do
shared_examples 'returning the right branches' do
let(:head_id) { repository.rugged.head.target.oid }
let(:new_branch) { head_id }
+ let(:utf8_branch) { 'branch-é' }
before do
repository.create_branch(new_branch, 'master')
+ repository.create_branch(utf8_branch, 'master')
end
after do
repository.delete_branch(new_branch)
+ repository.delete_branch(utf8_branch)
end
it 'displays that branch' do
- expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch)
+ expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch, utf8_branch)
end
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 37a0bf1ad36..95b63fc91fc 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -55,7 +55,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_archive feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -100,7 +100,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_send_git_patch feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -173,7 +173,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_send_git_diff feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -455,7 +455,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_raw_show feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index ff5a6f63010..f73f44ca0ad 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1961,6 +1961,17 @@ describe MergeRequest do
expect(subject.merge_request_diff_for(merge_request_diff3.head_commit_sha)).to eq(merge_request_diff3)
end
end
+
+ it 'runs a single query on the initial call, and none afterwards' do
+ expect { subject.merge_request_diff_for(merge_request_diff1.diff_refs) }
+ .not_to exceed_query_limit(1)
+
+ expect { subject.merge_request_diff_for(merge_request_diff2.diff_refs) }
+ .not_to exceed_query_limit(0)
+
+ expect { subject.merge_request_diff_for(merge_request_diff3.head_commit_sha) }
+ .not_to exceed_query_limit(0)
+ end
end
describe '#version_params_for' do
diff --git a/spec/policies/protected_branch_policy_spec.rb b/spec/policies/protected_branch_policy_spec.rb
new file mode 100644
index 00000000000..b39de42d721
--- /dev/null
+++ b/spec/policies/protected_branch_policy_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe ProtectedBranchPolicy do
+ let(:user) { create(:user) }
+ let(:name) { 'feature' }
+ let(:protected_branch) { create(:protected_branch, name: name) }
+ let(:project) { protected_branch.project }
+
+ subject { described_class.new(user, protected_branch) }
+
+ it 'branches can be updated via project masters' do
+ project.add_master(user)
+
+ is_expected.to be_allowed(:update_protected_branch)
+ end
+
+ it "branches can't be updated by guests" do
+ project.add_guest(user)
+
+ is_expected.to be_disallowed(:update_protected_branch)
+ end
+end
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index 1d23e023bb6..576fde46615 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -193,6 +193,19 @@ describe API::ProtectedBranches do
expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
end
end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ post post_endpoint, name: branch_name
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
end
context 'when authenticated as a guest' do
@@ -209,18 +222,20 @@ describe API::ProtectedBranches do
end
describe "DELETE /projects/:id/protected_branches/unprotect/:branch" do
+ let(:delete_endpoint) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) }
+
before do
project.add_master(user)
end
it "unprotects a single branch" do
- delete api("/projects/#{project.id}/protected_branches/#{branch_name}", user)
+ delete delete_endpoint
expect(response).to have_gitlab_http_status(204)
end
it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) }
+ let(:request) { delete_endpoint }
end
it "returns 404 if branch does not exist" do
@@ -229,11 +244,24 @@ describe API::ProtectedBranches do
expect(response).to have_gitlab_http_status(404)
end
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ delete delete_endpoint
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
context 'when branch has a wildcard in its name' do
let(:protected_name) { 'feature*' }
it "unprotects a wildcard branch" do
- delete api("/projects/#{project.id}/protected_branches/#{branch_name}", user)
+ delete delete_endpoint
expect(response).to have_gitlab_http_status(204)
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index f7c04c19903..1e6bd993c08 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -243,17 +243,34 @@ describe 'Git LFS API and storage' do
it_behaves_like 'responds with a file'
context 'when LFS uses object storage' do
- let(:before_get) do
- stub_lfs_object_storage
- lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ context 'when proxy download is enabled' do
+ let(:before_get) do
+ stub_lfs_object_storage(proxy_download: true)
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
+
+ it 'responds with redirect' do
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'responds with the workhorse send-url' do
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
+ end
end
- it 'responds with redirect' do
- expect(response).to have_gitlab_http_status(302)
- end
+ context 'when proxy download is disabled' do
+ let(:before_get) do
+ stub_lfs_object_storage(proxy_download: false)
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
+
+ it 'responds with redirect' do
+ expect(response).to have_gitlab_http_status(302)
+ end
- it 'responds with the file location' do
- expect(response.location).to include(lfs_object.reload.file.path)
+ it 'responds with the file location' do
+ expect(response.location).to include(lfs_object.reload.file.path)
+ end
end
end
end
@@ -962,22 +979,61 @@ describe 'Git LFS API and storage' do
end
context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- put_authorize
+ shared_examples 'a valid response' do
+ before do
+ put_authorize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'uses the gitlab-workhorse content type' do
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
+ shared_examples 'a local file' do
+ it_behaves_like 'a valid response' do
+ it 'responds with status 200, location of lfs store and object details' do
+ expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
end
- it 'uses the gitlab-workhorse content type' do
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ context 'when using local storage' do
+ it_behaves_like 'a local file'
end
- it 'responds with status 200, location of lfs store and object details' do
- expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path)
- expect(json_response['LfsOid']).to eq(sample_oid)
- expect(json_response['LfsSize']).to eq(sample_size)
+ context 'when using remote storage' do
+ context 'when direct upload is enabled' do
+ before do
+ stub_lfs_object_storage(enabled: true, direct_upload: true)
+ end
+
+ it_behaves_like 'a valid response' do
+ it 'responds with status 200, location of lfs remote store and object details' do
+ expect(json_response['TempPath']).to be_nil
+ expect(json_response['RemoteObject']).to have_key('ID')
+ expect(json_response['RemoteObject']).to have_key('GetURL')
+ expect(json_response['RemoteObject']).to have_key('StoreURL')
+ expect(json_response['RemoteObject']).to have_key('DeleteURL')
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ stub_lfs_object_storage(enabled: true, direct_upload: false)
+ end
+
+ it_behaves_like 'a local file'
+ end
end
end
@@ -1009,26 +1065,81 @@ describe 'Git LFS API and storage' do
end
context 'with object storage enabled' do
- before do
- stub_lfs_object_storage(background_upload: true)
+ context 'and direct upload enabled' do
+ let!(:fog_connection) do
+ stub_lfs_object_storage(direct_upload: true)
+ end
+
+ ['123123', '../../123123'].each do |remote_id|
+ context "with invalid remote_id: #{remote_id}" do
+ subject do
+ put_finalize_with_args('file.remote_id' => remote_id)
+ end
+
+ it 'responds with status 403' do
+ subject
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'with valid remote_id' do
+ before do
+ fog_connection.directories.get('lfs-objects').files.create(
+ key: 'tmp/upload/12312300',
+ body: 'content'
+ )
+ end
+
+ subject do
+ put_finalize_with_args(
+ 'file.remote_id' => '12312300',
+ 'file.name' => 'name')
+ end
+
+ it 'responds with status 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'schedules migration of file to object storage' do
+ subject
+
+ expect(LfsObject.last.projects).to include(project)
+ end
+
+ it 'have valid file' do
+ subject
+
+ expect(LfsObject.last.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(LfsObject.last.file).to be_exists
+ end
+ end
end
- it 'schedules migration of file to object storage' do
- expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric))
+ context 'and background upload enabled' do
+ before do
+ stub_lfs_object_storage(background_upload: true)
+ end
- put_finalize(with_tempfile: true)
+ it 'schedules migration of file to object storage' do
+ expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric))
+
+ put_finalize(with_tempfile: true)
+ end
end
end
end
context 'invalid tempfiles' do
- it 'rejects slashes in the tempfile name (path traversal' do
- put_finalize('foo/bar')
- expect(response).to have_gitlab_http_status(403)
+ before do
+ lfs_object.destroy
end
- it 'rejects tempfile names that do not start with the oid' do
- put_finalize("foo#{sample_oid}")
+ it 'rejects slashes in the tempfile name (path traversal)' do
+ put_finalize('../bar', with_tempfile: true)
expect(response).to have_gitlab_http_status(403)
end
end
@@ -1118,7 +1229,7 @@ describe 'Git LFS API and storage' do
end
it 'with location of lfs store and object details' do
- expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path)
+ expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end
@@ -1221,21 +1332,28 @@ describe 'Git LFS API and storage' do
end
def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false)
- setup_tempfile(lfs_tmp) if with_tempfile
+ upload_path = LfsObjectUploader.workhorse_local_upload_path
+ file_path = upload_path + '/' + lfs_tmp if lfs_tmp
- put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", nil,
- headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp).compact
- end
+ if with_tempfile
+ FileUtils.mkdir_p(upload_path)
+ FileUtils.touch(file_path)
+ end
- def lfs_tmp_file
- "#{sample_oid}012345678"
+ args = {
+ 'file.path' => file_path,
+ 'file.name' => File.basename(file_path)
+ }.compact
+
+ put_finalize_with_args(args)
end
- def setup_tempfile(lfs_tmp)
- upload_path = LfsObjectUploader.workhorse_upload_path
+ def put_finalize_with_args(args)
+ put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", args, headers
+ end
- FileUtils.mkdir_p(upload_path)
- FileUtils.touch(File.join(upload_path, lfs_tmp))
+ def lfs_tmp_file
+ "#{sample_oid}012345678"
end
end
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 53b3e5e365d..786493c3577 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -35,5 +35,18 @@ describe ProtectedBranches::CreateService do
expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
+
+ context 'when a policy restricts rule creation' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents creation of the protected branch rule" do
+ expect do
+ service.execute
+ end.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
end
end
diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb
new file mode 100644
index 00000000000..4a391b6c25c
--- /dev/null
+++ b/spec/services/protected_branches/destroy_service_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe ProtectedBranches::DestroyService do
+ let(:protected_branch) { create(:protected_branch) }
+ let(:project) { protected_branch.project }
+ let(:user) { project.owner }
+
+ describe '#execute' do
+ subject(:service) { described_class.new(project, user) }
+
+ it 'destroys a protected branch' do
+ service.execute(protected_branch)
+
+ expect(protected_branch).to be_destroyed
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ expect do
+ service.execute(protected_branch)
+ end.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+ end
+end
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
index 9fa5983db66..3f6f8e09565 100644
--- a/spec/services/protected_branches/update_service_spec.rb
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -22,5 +22,16 @@ describe ProtectedBranches::UpdateService do
expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
+
+ context 'when a policy restricts rule creation' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents creation of the protected branch rule" do
+ expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
end
end
diff --git a/spec/services/protected_tags/destroy_service_spec.rb b/spec/services/protected_tags/destroy_service_spec.rb
new file mode 100644
index 00000000000..e12f53a2221
--- /dev/null
+++ b/spec/services/protected_tags/destroy_service_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe ProtectedTags::DestroyService do
+ let(:protected_tag) { create(:protected_tag) }
+ let(:project) { protected_tag.project }
+ let(:user) { project.owner }
+
+ describe '#execute' do
+ subject(:service) { described_class.new(project, user) }
+
+ it 'destroy a protected tag' do
+ service.execute(protected_tag)
+
+ expect(protected_tag).to be_destroyed
+ end
+ end
+end
diff --git a/spec/support/stub_object_storage.rb b/spec/support/stub_object_storage.rb
index 1a0a2feb27d..6e88641da42 100644
--- a/spec/support/stub_object_storage.rb
+++ b/spec/support/stub_object_storage.rb
@@ -1,17 +1,22 @@
module StubConfiguration
def stub_object_storage_uploader(
- config:, uploader:, remote_directory:,
+ config:,
+ uploader:,
+ remote_directory:,
enabled: true,
proxy_download: false,
- background_upload: false)
- Fog.mock!
-
+ background_upload: false,
+ direct_upload: false
+ )
allow(config).to receive(:enabled) { enabled }
allow(config).to receive(:proxy_download) { proxy_download }
allow(config).to receive(:background_upload) { background_upload }
+ allow(config).to receive(:direct_upload) { direct_upload }
return unless enabled
+ Fog.mock!
+
::Fog::Storage.new(uploader.object_store_credentials).tap do |connection|
begin
connection.directories.create(key: remote_directory)
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
index 60e35dcf235..4fba122cce1 100644
--- a/spec/uploaders/gitlab_uploader_spec.rb
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -27,7 +27,7 @@ describe GitlabUploader do
describe '#file_cache_storage?' do
context 'when file storage is used' do
before do
- uploader_class.cache_storage(:file)
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::File }
end
it { is_expected.to be_file_cache_storage }
@@ -35,7 +35,7 @@ describe GitlabUploader do
context 'when is remote storage' do
before do
- uploader_class.cache_storage(:fog)
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::Fog }
end
it { is_expected.not_to be_file_cache_storage }
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 489b6707c6e..1d406c71955 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -21,11 +21,11 @@ describe ObjectStorage do
let(:object) { build_stubbed(:user) }
let(:uploader) { uploader_class.new(object, :file) }
- before do
- allow(uploader_class).to receive(:object_store_enabled?).and_return(true)
- end
-
describe '#object_store=' do
+ before do
+ allow(uploader_class).to receive(:object_store_enabled?).and_return(true)
+ end
+
it "reload the local storage" do
uploader.object_store = described_class::Store::LOCAL
expect(uploader.file_storage?).to be_truthy
@@ -35,28 +35,28 @@ describe ObjectStorage do
uploader.object_store = described_class::Store::REMOTE
expect(uploader.file_storage?).to be_falsey
end
- end
- context 'object_store is Store::LOCAL' do
- before do
- uploader.object_store = described_class::Store::LOCAL
- end
+ context 'object_store is Store::LOCAL' do
+ before do
+ uploader.object_store = described_class::Store::LOCAL
+ end
- describe '#store_dir' do
- it 'is the composition of (base_dir, dynamic_segment)' do
- expect(uploader.store_dir).to start_with("uploads/-/system/user/")
+ describe '#store_dir' do
+ it 'is the composition of (base_dir, dynamic_segment)' do
+ expect(uploader.store_dir).to start_with("uploads/-/system/user/")
+ end
end
end
- end
- context 'object_store is Store::REMOTE' do
- before do
- uploader.object_store = described_class::Store::REMOTE
- end
+ context 'object_store is Store::REMOTE' do
+ before do
+ uploader.object_store = described_class::Store::REMOTE
+ end
- describe '#store_dir' do
- it 'is the composition of (dynamic_segment)' do
- expect(uploader.store_dir).to start_with("user/")
+ describe '#store_dir' do
+ it 'is the composition of (dynamic_segment)' do
+ expect(uploader.store_dir).to start_with("user/")
+ end
end
end
end
@@ -92,7 +92,7 @@ describe ObjectStorage do
describe '#file_cache_storage?' do
context 'when file storage is used' do
before do
- uploader_class.cache_storage(:file)
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::File }
end
it { expect(uploader).to be_file_cache_storage }
@@ -100,7 +100,7 @@ describe ObjectStorage do
context 'when is remote storage' do
before do
- uploader_class.cache_storage(:fog)
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::Fog }
end
it { expect(uploader).not_to be_file_cache_storage }
@@ -298,7 +298,9 @@ describe ObjectStorage do
let(:remote_directory) { 'directory' }
before do
- uploader_class.storage_options double(object_store: double(remote_directory: remote_directory))
+ allow(uploader_class).to receive(:options) do
+ double(object_store: double(remote_directory: remote_directory))
+ end
end
subject { uploader.fog_directory }
@@ -310,7 +312,9 @@ describe ObjectStorage do
let(:connection) { Settingslogic.new("provider" => "AWS") }
before do
- uploader_class.storage_options double(object_store: double(connection: connection))
+ allow(uploader_class).to receive(:options) do
+ double(object_store: double(connection: connection))
+ end
end
subject { uploader.fog_credentials }
@@ -323,4 +327,304 @@ describe ObjectStorage do
it { is_expected.to eq(false) }
end
+
+ describe '.workhorse_authorize' do
+ subject { uploader_class.workhorse_authorize }
+
+ before do
+ # ensure that we use regular Fog libraries
+ # other tests might call `Fog.mock!` and
+ # it will make tests to fail
+ Fog.unmock!
+ end
+
+ shared_examples 'uses local storage' do
+ it "returns temporary path" do
+ is_expected.to have_key(:TempPath)
+
+ expect(subject[:TempPath]).to start_with(uploader_class.root)
+ expect(subject[:TempPath]).to include(described_class::TMP_UPLOAD_PATH)
+ end
+
+ it "does not return remote store" do
+ is_expected.not_to have_key('RemoteObject')
+ end
+ end
+
+ shared_examples 'uses remote storage' do
+ it "returns remote store" do
+ is_expected.to have_key(:RemoteObject)
+
+ expect(subject[:RemoteObject]).to have_key(:ID)
+ expect(subject[:RemoteObject]).to have_key(:GetURL)
+ expect(subject[:RemoteObject]).to have_key(:DeleteURL)
+ expect(subject[:RemoteObject]).to have_key(:StoreURL)
+ expect(subject[:RemoteObject][:GetURL]).to include(described_class::TMP_UPLOAD_PATH)
+ expect(subject[:RemoteObject][:DeleteURL]).to include(described_class::TMP_UPLOAD_PATH)
+ expect(subject[:RemoteObject][:StoreURL]).to include(described_class::TMP_UPLOAD_PATH)
+ end
+
+ it "does not return local store" do
+ is_expected.not_to have_key('TempPath')
+ end
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:enabled) { false }
+ end
+
+ it_behaves_like 'uses local storage'
+ end
+
+ context 'when object storage is enabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:enabled) { true }
+ end
+
+ context 'when direct upload is enabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:direct_upload) { true }
+ end
+
+ context 'uses AWS' do
+ before do
+ expect(uploader_class).to receive(:object_store_credentials) do
+ { provider: "AWS",
+ aws_access_key_id: "AWS_ACCESS_KEY_ID",
+ aws_secret_access_key: "AWS_SECRET_ACCESS_KEY",
+ region: "eu-central-1" }
+ end
+ end
+
+ it_behaves_like 'uses remote storage' do
+ let(:storage_url) { "https://uploads.s3-eu-central-1.amazonaws.com/" }
+
+ it 'returns links for S3' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+
+ context 'uses Google' do
+ before do
+ expect(uploader_class).to receive(:object_store_credentials) do
+ { provider: "Google",
+ google_storage_access_key_id: 'ACCESS_KEY_ID',
+ google_storage_secret_access_key: 'SECRET_ACCESS_KEY' }
+ end
+ end
+
+ it_behaves_like 'uses remote storage' do
+ let(:storage_url) { "https://storage.googleapis.com/uploads/" }
+
+ it 'returns links for Google Cloud' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+
+ context 'uses GDK/minio' do
+ before do
+ expect(uploader_class).to receive(:object_store_credentials) do
+ { provider: "AWS",
+ aws_access_key_id: "AWS_ACCESS_KEY_ID",
+ aws_secret_access_key: "AWS_SECRET_ACCESS_KEY",
+ endpoint: 'http://127.0.0.1:9000',
+ path_style: true,
+ region: "gdk" }
+ end
+ end
+
+ it_behaves_like 'uses remote storage' do
+ let(:storage_url) { "http://127.0.0.1:9000/uploads/" }
+
+ it 'returns links for S3' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:direct_upload) { false }
+ end
+
+ it_behaves_like 'uses local storage'
+ end
+ end
+ end
+
+ describe '#store_workhorse_file!' do
+ subject do
+ uploader.store_workhorse_file!(params, :file)
+ end
+
+ context 'when local file is used' do
+ context 'when valid file is used' do
+ let(:target_path) do
+ File.join(uploader_class.root, uploader_class::TMP_UPLOAD_PATH)
+ end
+
+ before do
+ FileUtils.mkdir_p(target_path)
+ end
+
+ context 'when no filename is specified' do
+ let(:params) do
+ { "file.path" => "test/file" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing filename/)
+ end
+ end
+
+ context 'when invalid file is specified' do
+ let(:file_path) do
+ File.join(target_path, "..", "test.file")
+ end
+
+ before do
+ FileUtils.touch(file_path)
+ end
+
+ let(:params) do
+ { "file.path" => file_path,
+ "file.name" => "my_file.txt" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file path/)
+ end
+ end
+
+ context 'when filename is specified' do
+ let(:params) do
+ { "file.path" => tmp_file,
+ "file.name" => "my_file.txt" }
+ end
+
+ let(:tmp_file) { Tempfile.new('filename', target_path) }
+
+ before do
+ FileUtils.touch(tmp_file)
+ end
+
+ after do
+ FileUtils.rm_f(tmp_file)
+ end
+
+ it 'succeeds' do
+ expect { subject }.not_to raise_error
+
+ expect(uploader).to be_exists
+ end
+
+ it 'proper path is being used' do
+ subject
+
+ expect(uploader.path).to start_with(uploader_class.root)
+ expect(uploader.path).to end_with("my_file.txt")
+ end
+
+ it 'source file to not exist' do
+ subject
+
+ expect(File.exist?(tmp_file.path)).to be_falsey
+ end
+ end
+ end
+ end
+
+ context 'when remote file is used' do
+ let!(:fog_connection) do
+ stub_uploads_object_storage(uploader_class)
+ end
+
+ context 'when valid file is used' do
+ context 'when no filename is specified' do
+ let(:params) do
+ { "file.remote_id" => "test/123123" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing filename/)
+ end
+ end
+
+ context 'when invalid file is specified' do
+ let(:params) do
+ { "file.remote_id" => "../test/123123",
+ "file.name" => "my_file.txt" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file path/)
+ end
+ end
+
+ context 'when non existing file is specified' do
+ let(:params) do
+ { "file.remote_id" => "test/12312300",
+ "file.name" => "my_file.txt" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing file/)
+ end
+ end
+
+ context 'when filename is specified' do
+ let(:params) do
+ { "file.remote_id" => "test/123123",
+ "file.name" => "my_file.txt" }
+ end
+
+ let!(:fog_file) do
+ fog_connection.directories.get('uploads').files.create(
+ key: 'tmp/upload/test/123123',
+ body: 'content'
+ )
+ end
+
+ it 'succeeds' do
+ expect { subject }.not_to raise_error
+
+ expect(uploader).to be_exists
+ end
+
+ it 'path to not be temporary' do
+ subject
+
+ expect(uploader.path).not_to be_nil
+ expect(uploader.path).not_to include('tmp/upload')
+ expect(uploader.url).to include('/my_file.txt')
+ end
+
+ it 'url is used' do
+ subject
+
+ expect(uploader.url).not_to be_nil
+ expect(uploader.url).to include('/my_file.txt')
+ end
+ end
+ end
+ end
+
+ context 'when no file is used' do
+ let(:params) { {} }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file/)
+ end
+ end
+ end
end
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/projects/ci/lints/show.html.haml_spec.rb
index ded320793ea..2f0cd38c14a 100644
--- a/spec/views/ci/lints/show.html.haml_spec.rb
+++ b/spec/views/projects/ci/lints/show.html.haml_spec.rb
@@ -1,12 +1,13 @@
require 'spec_helper'
-describe 'ci/lints/show' do
+describe 'projects/ci/lints/show' do
include Devise::Test::ControllerHelpers
+ let(:project) { create(:project, :repository) }
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
describe 'XSS protection' do
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
-
before do
+ assign(:project, project)
assign(:status, true)
assign(:builds, config_processor.builds)
assign(:stages, config_processor.stages)
@@ -48,22 +49,21 @@ describe 'ci/lints/show' do
end
end
- let(:content) do
- {
- build_template: {
- script: './build.sh',
- tags: ['dotnet'],
- only: ['test@dude/repo'],
- except: ['deploy'],
- environment: 'testing'
+ context 'when the content is valid' do
+ let(:content) do
+ {
+ build_template: {
+ script: './build.sh',
+ tags: ['dotnet'],
+ only: ['test@dude/repo'],
+ except: ['deploy'],
+ environment: 'testing'
+ }
}
- }
- end
-
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
+ end
- context 'when the content is valid' do
before do
+ assign(:project, project)
assign(:status, true)
assign(:builds, config_processor.builds)
assign(:stages, config_processor.stages)
@@ -83,6 +83,7 @@ describe 'ci/lints/show' do
context 'when the content is invalid' do
before do
+ assign(:project, project)
assign(:status, false)
assign(:error, 'Undefined error')
end
diff --git a/yarn.lock b/yarn.lock
index af7bda5d562..584951b5da0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3605,7 +3605,7 @@ fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
-fsevents@^1.0.0:
+fsevents@^1.0.0, fsevents@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
dependencies: