diff options
219 files changed, 3182 insertions, 1028 deletions
diff --git a/.flayignore b/.flayignore index 0c4eee10ffa..7faa6c7bb90 100644 --- a/.flayignore +++ b/.flayignore @@ -10,3 +10,7 @@ lib/gitlab/background_migration/* app/models/project_services/kubernetes_service.rb lib/gitlab/workhorse.rb lib/gitlab/ci/trace/chunked_io.rb +lib/gitlab/gitaly_client/ref_service.rb +lib/gitlab/gitaly_client/commit_service.rb +lib/gitlab/git/commit.rb +lib/gitlab/git/tag.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b1445feee58..84d8e69b84e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -189,7 +189,7 @@ stages: <<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *use-pg variables: - CREATE_DB_USER: "true" + SETUP_DB: "false" script: # Manually clone gitlab-test and only seed this project in # db/fixtures/development/04_project.rb thanks to SIZE=1 below @@ -233,7 +233,7 @@ stages: .migration-paths: &migration-paths <<: *dedicated-no-docs-and-no-qa-pull-cache-job variables: - CREATE_DB_USER: "true" + SETUP_DB: "false" script: - git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0 - git checkout -f FETCH_HEAD @@ -242,7 +242,7 @@ stages: - cp config/gitlab.yml.example config/gitlab.yml - bundle exec rake db:drop db:create db:schema:load db:seed_fu - date - - git checkout $CI_COMMIT_SHA + - git checkout -f $CI_COMMIT_SHA - bundle install $BUNDLE_INSTALL_FLAGS - date - . scripts/prepare_build.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5447ebbdd8c..383d13656e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -168,7 +168,7 @@ hits. They are not always necessary, but very convenient. If you are an expert in a particular area, it makes it easier to find issues to work on. You can also subscribe to those labels to receive an email each time an -issue is labelled with a subject label corresponding to your expertise. +issue is labeled with a subject label corresponding to your expertise. Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api, ~issues, ~"merge requests", ~labels, and ~"container registry". @@ -296,7 +296,24 @@ any potential community contributor to @-mention per above. ## Implement design & UI elements -Please see the [UX Guide for GitLab]. +For guidance on UX implementation at GitLab, please refer to our [Design System](https://design.gitlab.com/). + +The UX team uses labels to manage their workflow. + +The ~"UX" label on an issue is a signal to the UX team that it will need UX attention. +To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/ux/) of the handbook. + +Once an issue has been worked on and is ready for development, a UXer applies the ~"UX ready" label to that issue. + +The UX team has a special type label called ~"design artifact". This label indicates that the final output +for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone. +Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is +needed until the solution has been decided. + +~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone. + +Once the ~"design artifact" issue has been completed, the UXer removes the ~"design artifact" label and applies the ~"UX ready" label. The Product Manager can use the +existing issue or decide to create a whole new issue for the purpose of development. ## Issue tracker diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 897e21587ed..7bb21aff834 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.100.0 +0.102.0 @@ -4,9 +4,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---- - -All Documentation content that resides under the doc/ directory of this -repository is licensed under Creative Commons: CC BY-SA 4.0. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/README.md b/README.md index 9c1aad65307..013cac75c46 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,8 @@ GitLab Community Edition (CE) is available freely under the MIT Expat license. All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. +All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. + ## Install a development environment To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index c2a35341eb2..fae580c091b 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -179,7 +179,7 @@ role="row" > <div - class="alert alert-danger alert-block append-bottom-0 table-section section-100" + class="alert alert-danger alert-block append-bottom-0" role="gridcell" > <div> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 4a645c827ad..81961fe3c57 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -5,7 +5,7 @@ import LoadingButton from '~/vue_shared/components/loading_button.vue'; import CommitMessageField from './message_field.vue'; import Actions from './actions.vue'; import SuccessMessage from './success_message.vue'; -import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants'; +import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants'; export default { components: { @@ -70,7 +70,7 @@ export default { ? this.$refs.formEl && this.$refs.formEl.offsetHeight : this.$refs.compactEl && this.$refs.compactEl.offsetHeight; - this.componentHeight = elHeight + COMMIT_ITEM_PADDING; + this.componentHeight = elHeight; }, enterTransition() { this.$nextTick(() => { @@ -78,7 +78,7 @@ export default { ? this.$refs.compactEl && this.$refs.compactEl.offsetHeight : this.$refs.formEl && this.$refs.formEl.offsetHeight; - this.componentHeight = elHeight + COMMIT_ITEM_PADDING; + this.componentHeight = elHeight; }); }, afterEndTransition() { diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index 14946f8c9fa..7bc865058c6 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -122,11 +122,11 @@ export default { <div class="file" :class="fileClass" + @click="clickFile" + role="button" > <div class="file-name" - @click="clickFile" - role="button" > <span class="ide-file-name str-truncated" diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 48d4cc43198..83fe22f40a4 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -5,8 +5,6 @@ export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33; export const MAX_WINDOW_HEIGHT_COMPACT = 750; -export const COMMIT_ITEM_PADDING = 32; - // Commit message textarea export const MAX_TITLE_LENGTH = 50; export const MAX_BODY_LENGTH = 72; diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 7183d0b50b2..a4081957207 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -93,10 +93,13 @@ export default { v-html="actionTextHtml" class="system-note-message"> </span> + <span class="system-note-separator"> + · + </span> <a :href="noteTimestampLink" @click="updateTargetNoteHash" - class="note-timestamp"> + class="note-timestamp system-note-separator"> <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js index a99ce0f1c36..5316d3e9f3c 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ +/* eslint-disable func-names, space-before-function-paren, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ import $ from 'jquery'; import _ from 'underscore'; @@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats'; const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; -const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; const hasProp = {}.hasOwnProperty; +const extend = function(child, parent) { for (const key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; export const ContributorsGraph = (function() { function ContributorsGraph() {} ContributorsGraph.prototype.MARGIN = { top: 20, - right: 20, + right: 10, bottom: 30, - left: 50 + left: 40 }; ContributorsGraph.prototype.x_domain = null; @@ -32,6 +32,12 @@ export const ContributorsGraph = (function() { ContributorsGraph.prototype.dates = []; + ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) { + const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); + const marginWidth = this.MARGIN.left + this.MARGIN.right; + return baseWidth - parentPaddingWidth - marginWidth; + }; + ContributorsGraph.set_x_domain = function(data) { return ContributorsGraph.prototype.x_domain = data; }; @@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) { function ContributorsMasterGraph(data1) { const $parentElement = $('#contributors-master'); - const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); this.data = data1; this.update_content = this.update_content.bind(this); - this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right); + this.width = this.determine_width($('.js-graphs-show').width(), $parentElement); this.height = 200; this.x = null; this.y = null; @@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) { } ContributorsMasterGraph.prototype.process_dates = function(data) { - var dates; - dates = this.get_dates(data); + const dates = this.get_dates(data); this.parse_dates(data); return ContributorsGraph.set_dates(dates); }; @@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.parse_dates = function(data) { - var parseDate; - parseDate = d3.timeParse("%Y-%m-%d"); + const parseDate = d3.timeParse("%Y-%m-%d"); return data.forEach(function(d) { return d.date = parseDate(d.date); }); @@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.create_svg = function() { - return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3.select("#contributors-master") + .append("svg") + .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) + .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr("class", "tint-box") + .append("g") + .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + return this.svg; }; ContributorsMasterGraph.prototype.create_area = function(x, y) { @@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) { extend(ContributorsAuthorGraph, superClass); function ContributorsAuthorGraph(data1) { + const $parentElements = $('.person'); + this.data = data1; // Don't split graph size in half for mobile devices. - if ($(window).width() < 768) { - this.width = $('.content').width() - 80; + if ($(window).width() < 790) { + this.width = this.determine_width($('.js-graphs-show').width(), $parentElements); } else { - this.width = ($('.content').width() / 2) - 100; + this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements); } this.height = 200; this.x = null; @@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) { ContributorsAuthorGraph.prototype.create_area = function(x, y) { return this.area = d3.area().x(function(d) { - var parseDate; - parseDate = d3.timeParse("%Y-%m-%d"); + const parseDate = d3.timeParse("%Y-%m-%d"); return x(parseDate(d)); }).y0(this.height).y1((function(_this) { return function(d) { @@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) { }; ContributorsAuthorGraph.prototype.create_svg = function() { - var persons = document.querySelectorAll('.person'); + const persons = document.querySelectorAll('.person'); this.list_item = persons[persons.length - 1]; - return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3.select(this.list_item) + .append("svg") + .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) + .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr("class", "spark") + .append("g") + .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + return this.svg; }; ContributorsAuthorGraph.prototype.draw_path = function(data) { diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index fd3491c7fe0..82b4ce083fb 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,11 +1,21 @@ <script> import $ from 'jquery'; -import tooltip from '../../../vue_shared/directives/tooltip'; -import Icon from '../../../vue_shared/components/icon.vue'; -import { dasherize } from '../../../lib/utils/text_utility'; -import eventHub from '../../event_hub'; +import axios from '~/lib/utils/axios_utils'; +import { dasherize } from '~/lib/utils/text_utility'; +import { __ } from '~/locale'; +import createFlash from '~/flash'; +import tooltip from '~/vue_shared/directives/tooltip'; +import Icon from '~/vue_shared/components/icon.vue'; + /** - * Renders either a cancel, retry or play icon pointing to the given path. + * Renders either a cancel, retry or play icon button and handles the post request + * + * Used in: + * - mr widget mini pipeline graph: `mr_widget_pipeline.vue` + * - pipelines table + * - pipelines table in merge request page + * - pipelines table in commit page + * - pipelines detail page in big graph */ export default { components: { @@ -32,16 +42,10 @@ export default { required: true, }, - requestFinishedFor: { - type: String, - required: false, - default: '', - }, }, data() { return { isDisabled: false, - linkRequested: '', }; }, @@ -51,19 +55,28 @@ export default { return `${actionIconDash} js-icon-${actionIconDash}`; }, }, - watch: { - requestFinishedFor() { - if (this.requestFinishedFor === this.linkRequested) { - this.isDisabled = false; - } - }, - }, methods: { + /** + * The request should not be handled here. + * However due to this component being used in several + * different apps it avoids repetition & complexity. + * + */ onClickAction() { $(this.$el).tooltip('hide'); - eventHub.$emit('postAction', this.link); - this.linkRequested = this.link; + this.isDisabled = true; + + axios.post(`${this.link}.json`) + .then(() => { + this.isDisabled = false; + this.$emit('pipelineActionRequestComplete'); + }) + .catch(() => { + this.isDisabled = false; + + createFlash(__('An error occurred while making the request.')); + }); }, }, }; @@ -80,6 +93,6 @@ btn-transparent ci-action-icon-container ci-action-icon-wrapper" data-container="body" :disabled="isDisabled" > - <icon :name="actionIcon" /> + <icon :name="actionIcon"/> </button> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index 4027d26098f..7bfe11ab8cd 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -42,11 +42,6 @@ export default { type: Object, required: true, }, - requestFinishedFor: { - type: String, - required: false, - default: '', - }, }, computed: { @@ -76,11 +71,15 @@ export default { e.stopPropagation(); }); }, + + pipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); + }, }, }; </script> <template> - <div class="ci-job-dropdown-container"> + <div class="ci-job-dropdown-container dropdown"> <button v-tooltip type="button" @@ -110,7 +109,7 @@ export default { <job-component :job="item" css-class-job-name="mini-pipeline-graph-dropdown-item" - :request-finished-for="requestFinishedFor" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 7b8a5edcbff..4ec67f6c01b 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -16,11 +16,6 @@ export default { type: Object, required: true, }, - requestFinishedFor: { - type: String, - required: false, - default: '', - }, }, computed: { @@ -51,6 +46,10 @@ export default { return className; }, + + refreshPipelineGraph() { + this.$emit('refreshPipelineGraph'); + }, }, }; </script> @@ -74,7 +73,7 @@ export default { :key="stage.name" :stage-connector-class="stageConnectorClass(index, stage)" :is-first-column="isFirstColumn(index)" - :request-finished-for="requestFinishedFor" + @refreshPipelineGraph="refreshPipelineGraph" /> </ul> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index c1f0f051b63..27b938c4985 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -46,11 +46,6 @@ export default { required: false, default: '', }, - requestFinishedFor: { - type: String, - required: false, - default: '', - }, }, computed: { status() { @@ -84,6 +79,11 @@ export default { return this.job.status && this.job.status.action && this.job.status.action.path; }, }, + methods: { + pipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); + }, + }, }; </script> <template> @@ -126,7 +126,7 @@ export default { :tooltip-text="status.action.title" :link="status.action.path" :action-icon="status.action.icon" - :request-finished-for="requestFinishedFor" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 5461fdbbadd..f32368947e8 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -29,12 +29,6 @@ export default { required: false, default: '', }, - - requestFinishedFor: { - type: String, - required: false, - default: '', - }, }, methods: { @@ -49,6 +43,10 @@ export default { buildConnnectorClass(index) { return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; }, + + pipelineActionRequestComplete() { + this.$emit('refreshPipelineGraph'); + }, }, }; </script> @@ -75,12 +73,13 @@ export default { v-if="job.size === 1" :job="job" css-class-job-name="build-content" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> <dropdown-job-component v-if="job.size > 1" :job="job" - :request-finished-for="requestFinishedFor" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </li> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 498a97851fa..0f671ceea21 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -9,6 +9,7 @@ import CommitComponent from '../../vue_shared/components/commit.vue'; import LoadingButton from '../../vue_shared/components/loading_button.vue'; import Icon from '../../vue_shared/components/icon.vue'; + import { PIPELINES_TABLE } from '../constants'; /** * Pipeline table row. @@ -46,6 +47,7 @@ required: true, }, }, + pipelinesTable: PIPELINES_TABLE, data() { return { isRetrying: false, @@ -297,6 +299,7 @@ v-for="(stage, index) in pipeline.details.stages" :key="index"> <pipeline-stage + :type="$options.pipelinesTable" :stage="stage" :update-dropdown="updateGraphDropdown" /> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index a65485c05eb..f9769815796 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -21,6 +21,7 @@ import Icon from '../../vue_shared/components/icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import JobComponent from './graph/job_component.vue'; import tooltip from '../../vue_shared/directives/tooltip'; +import { PIPELINES_TABLE } from '../constants'; export default { components: { @@ -44,6 +45,12 @@ export default { required: false, default: false, }, + + type: { + type: String, + required: false, + default: '', + }, }, data() { @@ -133,6 +140,16 @@ export default { isDropdownOpen() { return this.$el.classList.contains('open'); }, + + pipelineActionRequestComplete() { + if (this.type === PIPELINES_TABLE) { + // warn the table to update + eventHub.$emit('refreshPipelinesTable'); + } else { + // close the dropdown in mr widget + $(this.$refs.dropdown).dropdown('toggle'); + } + }, }, }; </script> @@ -151,6 +168,7 @@ export default { id="stageDropdown" aria-haspopup="true" aria-expanded="false" + ref="dropdown" > <span @@ -188,6 +206,7 @@ export default { <job-component :job="job" css-class-job-name="mini-pipeline-graph-dropdown-item" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </li> </ul> diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index b384c7500e7..eaa11a84cb9 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -1,2 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export export const CANCEL_REQUEST = 'CANCEL_REQUEST'; +export const PIPELINES_TABLE = 'PIPELINES_TABLE'; diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index de0faf181e5..30b1eee186d 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -55,11 +55,13 @@ export default { eventHub.$on('postAction', this.postAction); eventHub.$on('retryPipeline', this.postAction); eventHub.$on('clickedDropdown', this.updateTable); + eventHub.$on('refreshPipelinesTable', this.fetchPipelines); }, beforeDestroy() { eventHub.$off('postAction', this.postAction); eventHub.$off('retryPipeline', this.postAction); eventHub.$off('clickedDropdown', this.updateTable); + eventHub.$off('refreshPipelinesTable', this.fetchPipelines); }, destroyed() { this.poll.stop(); diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 04fe7958fe6..b49a16a87e6 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -25,30 +25,14 @@ export default () => { data() { return { mediator, - requestFinishedFor: null, }; }, - created() { - eventHub.$on('postAction', this.postAction); - }, - beforeDestroy() { - eventHub.$off('postAction', this.postAction); - }, methods: { - postAction(action) { - // Click was made, reset this variable - this.requestFinishedFor = null; - - this.mediator.service - .postAction(action) - .then(() => { - this.mediator.refreshPipeline(); - this.requestFinishedFor = action; - }) - .catch(() => { - this.requestFinishedFor = action; - Flash(__('An error occurred while making the request.')); - }); + requestRefreshPipelineGraph() { + // When an action is clicked + // (wether in the dropdown or in the main nodes, we refresh the big graph) + this.mediator.refreshPipeline() + .catch(() => Flash(__('An error occurred while making the request.'))); }, }, render(createElement) { @@ -56,7 +40,9 @@ export default () => { props: { isLoading: this.mediator.state.isLoading, pipeline: this.mediator.store.state.pipeline, - requestFinishedFor: this.requestFinishedFor, + }, + on: { + refreshPipelineGraph: this.requestRefreshPipelineGraph, }, }); }, diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index a4d10850471..78f7353eb0d 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts { super(); Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project')); - Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity')); + Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity')); Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree')); Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits')); Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds')); @@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts { Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues')); Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards')); Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests')); - Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos')); Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki')); Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); + Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes')); + Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments')); Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); this.enabledHelp.push('.hidden-shortcut.project'); diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue index bec4e7c99b6..368eeb6c453 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -40,7 +40,7 @@ export default { :class="cssClass" :title="tooltipTitle(time)" :data-placement="tooltipPlacement" - data-container="body"> - {{ timeFormated(time) }} + data-container="body" + v-text="timeFormated(time)"> </time> </template> diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index f1a8a46dda4..45517416e93 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -279,251 +279,14 @@ ul.indent-list { padding: 10px 0 0 30px; } - // Specific styles for tree list @keyframes spin-avatar { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } -.groups-list-tree-container { - .has-no-search-results { - text-align: center; - padding: $gl-padding; - font-style: italic; - color: $well-light-text-color; - } - - > .group-list-tree > .group-row.has-children:first-child { - border-top: 0; - } -} - -.group-list-tree { - .avatar-container.content-loading { - position: relative; - - > a, - > a .avatar { - height: 100%; - border-radius: 50%; - } - - > a { - padding: 2px; - - .avatar { - border: 2px solid $white-normal; - - &.identicon { - line-height: 15px; - } - } - } - - &::after { - content: ""; - position: absolute; - height: 100%; - width: 100%; - background-color: transparent; - border: 2px outset $kdb-border; - border-radius: 50%; - animation: spin-avatar 3s infinite linear; - } - } - - .folder-toggle-wrap { - float: left; - line-height: $list-text-height; - font-size: 0; - - span { - font-size: $gl-font-size; - } - } - - .folder-caret, - .item-type-icon { - display: inline-block; - } - - .folder-caret { - width: 15px; - - svg { - margin-bottom: 2px; - } - } - - .item-type-icon { - margin-top: 2px; - width: 20px; - } - - > .group-row:not(.has-children) { - .folder-caret { - opacity: 0; - } - } - - .content-list li:last-child { - padding-bottom: 0; - } - - .group-list-tree { - margin-bottom: 0; - margin-left: 30px; - position: relative; - - &::before { - content: ''; - display: block; - width: 0; - position: absolute; - top: 5px; - bottom: 0; - left: -16px; - border-left: 2px solid $border-white-normal; - } - - .group-row { - position: relative; - - &::before { - content: ""; - display: block; - width: 10px; - height: 0; - border-top: 2px solid $border-white-normal; - position: absolute; - top: 30px; - left: -16px; - } - - &:last-child::before { - background: $white-light; - height: auto; - top: 30px; - bottom: 0; - } - - &.being-removed { - opacity: 0.5; - } - } - } - - .group-row { - padding: 0; - - &.has-children { - border-top: 0; - } - - &:first-child { - border-top: 1px solid $white-normal; - } - - &:last-of-type { - .group-row-contents:not(:hover) { - border-bottom: 1px solid transparent; - } - } - } - - .group-row-contents { - padding: 10px 10px 8px; - border-top: solid 1px transparent; - border-bottom: solid 1px $white-normal; - - &:hover { - border-color: $row-hover-border; - background-color: $row-hover; - cursor: pointer; - } - - .avatar-container > a { - width: 100%; - text-decoration: none; - } - - &.has-more-items { - display: block; - padding: 20px 10px; - } - - .stats { - position: relative; - line-height: 46px; - - > span { - display: inline-flex; - align-items: center; - height: 16px; - min-width: 30px; - } - - > span:last-child { - margin-right: 0; - } - - .stat-value { - margin: 2px 0 0 5px; - } - } - - .controls { - margin-left: 5px; - - > .btn { - margin-right: $btn-xs-side-margin; - } - } - } - - .project-row-contents .stats { - line-height: inherit; - - > span:first-child { - margin-left: 25px; - } - - .item-visibility { - margin-right: 0; - } - - .last-updated { - position: absolute; - right: 12px; - min-width: 250px; - text-align: right; - color: $gl-text-color-secondary; - } - } -} - .namespace-title { .tooltip-inner { max-width: 350px; } } - -ul.group-list-tree { - li.group-row { - > .group-row-contents .title { - line-height: $list-text-height; - } - - &.has-description > .group-row-contents .title { - line-height: inherit; - } - } -} - -.js-groups-list-holder { - .groups-list-loading { - font-size: 34px; - text-align: center; - } -} diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index 606d4675f19..430633bb01b 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -67,7 +67,8 @@ padding: 8px 40px; } - .embed-toggle { + .embed-toggle, + .snippet-clipboard-btn { height: 35px; } } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 011d38532b4..6bb40bae9ed 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -1,5 +1,3 @@ -@import './issues/issue_count_badge'; - [v-cloak] { display: none; } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 6ee8b33bd39..409b7285f82 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -18,6 +18,10 @@ .group-row { @include basic-list-stats; + + .description p { + margin-bottom: 0; + } } .ldap-group-links { @@ -237,3 +241,231 @@ overflow-y: unset; } } + +.groups-list-tree-container { + .has-no-search-results { + text-align: center; + padding: $gl-padding; + font-style: italic; + color: $well-light-text-color; + } + + > .group-list-tree > .group-row.has-children:first-child { + border-top: 0; + } +} + +.group-list-tree { + .avatar-container.content-loading { + position: relative; + + > a, + > a .avatar { + height: 100%; + border-radius: 50%; + } + + > a { + padding: 2px; + + .avatar { + border: 2px solid $white-normal; + + &.identicon { + line-height: 15px; + } + } + } + + &::after { + content: ""; + position: absolute; + height: 100%; + width: 100%; + background-color: transparent; + border: 2px outset $kdb-border; + border-radius: 50%; + animation: spin-avatar 3s infinite linear; + } + } + + .folder-toggle-wrap { + float: left; + line-height: $list-text-height; + font-size: 0; + + span { + font-size: $gl-font-size; + } + } + + .folder-caret, + .item-type-icon { + display: inline-block; + } + + .folder-caret { + width: 15px; + + svg { + margin-bottom: 2px; + } + } + + .item-type-icon { + margin-top: 2px; + width: 20px; + } + + > .group-row:not(.has-children) { + .folder-caret { + opacity: 0; + } + } + + .content-list li:last-child { + padding-bottom: 0; + } + + .group-list-tree { + margin-bottom: 0; + margin-left: 30px; + position: relative; + + &::before { + content: ''; + display: block; + width: 0; + position: absolute; + top: 5px; + bottom: 0; + left: -16px; + border-left: 2px solid $border-white-normal; + } + + .group-row { + position: relative; + + &::before { + content: ""; + display: block; + width: 10px; + height: 0; + border-top: 2px solid $border-white-normal; + position: absolute; + top: 30px; + left: -16px; + } + + &:last-child::before { + background: $white-light; + height: auto; + top: 30px; + bottom: 0; + } + + &.being-removed { + opacity: 0.5; + } + } + } + + .group-row { + padding: 0; + + &.has-children { + border-top: 0; + } + + &:first-child { + border-top: 1px solid $white-normal; + } + } + + .group-row-contents { + padding: $gl-padding-top; + + &:hover { + border-color: $row-hover-border; + background-color: $row-hover; + cursor: pointer; + } + + .avatar-container > a { + width: 100%; + text-decoration: none; + } + + &.has-more-items { + display: block; + padding: 20px 10px; + } + + .stats { + position: relative; + line-height: 46px; + + > span { + display: inline-flex; + align-items: center; + height: 16px; + min-width: 30px; + } + + > span:last-child { + margin-right: 0; + } + + .stat-value { + margin: 2px 0 0 5px; + } + } + + .controls { + margin-left: 5px; + + > .btn { + margin-right: $btn-xs-side-margin; + } + } + } + + .project-row-contents .stats { + line-height: inherit; + + > span:first-child { + margin-left: 25px; + } + + .item-visibility { + margin-right: 0; + } + + .last-updated { + position: absolute; + right: 12px; + min-width: 250px; + text-align: right; + color: $gl-text-color-secondary; + } + } +} + +ul.group-list-tree { + li.group-row { + > .group-row-contents .title { + line-height: $list-text-height; + } + + &.has-description > .group-row-contents .title { + line-height: inherit; + } + } +} + +.js-groups-list-holder { + .groups-list-loading { + font-size: 34px; + text-align: center; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b2dad4a358a..a8110f069d4 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -197,9 +197,21 @@ } &.assignee { - .author_link:hover { - .author { - text-decoration: underline; + .author_link { + display: block; + padding-left: 42px; + position: relative; + + &:hover { + .author { + text-decoration: underline; + } + } + + .avatar { + left: 0; + position: absolute; + top: 0; } } } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index b9390450477..0d17b9bae7e 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -1,5 +1,3 @@ -@import "./issues/issue_count_badge"; - .issues-list { .issue { padding: 10px 0 10px $gl-padding; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 42fe319b931..feee964f9bb 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -455,6 +455,10 @@ ul.notes { white-space: normal; } + .system-note-separator { + color: $gl-text-color-disabled; + } + a:hover { text-decoration: underline; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 02803e7b040..1264d977b2f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -66,13 +66,9 @@ } } - .btn-group { - &.open { - .btn-default { - background-color: $white-normal; - border-color: $border-white-normal; - } - } + .btn-group.open .btn-default { + background-color: $white-normal; + border-color: $border-white-normal; } .btn .text-center { @@ -361,16 +357,14 @@ &:not(:first-child) { margin-left: 44px; - .left-connector { - &::before { - content: ''; - position: absolute; - top: 48%; - left: -44px; - border-top: 2px solid $border-color; - width: 44px; - height: 1px; - } + .left-connector::before { + content: ''; + position: absolute; + top: 48%; + left: -44px; + border-top: 2px solid $border-color; + width: 44px; + height: 1px; } } } @@ -386,22 +380,16 @@ &:last-child { .build { // Remove right connecting horizontal line from first build in last stage - &:first-child { - &::after { - border: 0; - } + &:first-child::after { + border: 0; } // Remove right curved connectors from all builds in last stage - &:not(:first-child) { - &::after { - border: 0; - } + &:not(:first-child)::after { + border: 0; } // Remove opposite curve - .curve { - &::before { - display: none; - } + .curve::before { + display: none; } } } @@ -409,16 +397,12 @@ &:first-child { .build { // Remove left curved connectors from all builds in first stage - &:not(:first-child) { - &::before { - border: 0; - } + &:not(:first-child)::before { + border: 0; } // Remove opposite curve - .curve { - &::after { - display: none; - } + .curve::after { + display: none; } } } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 00457717f00..175d2779bb7 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -39,12 +39,15 @@ .ide-file-list { flex: 1; + padding-left: $gl-padding; + padding-right: $gl-padding; + padding-bottom: $grid-size; .file { cursor: pointer; &.file-open { - background: $link-active-background; + background: $white-normal; } &.file-active { @@ -84,12 +87,11 @@ .ide-new-btn { display: none; - margin-right: -8px; } &:hover, &:focus { - background: $link-active-background; + background: $white-normal; .ide-new-btn { display: block; @@ -111,12 +113,11 @@ } } -.file-name, -.file-col-commit-message { +.file-name { display: flex; overflow: visible; align-items: center; - padding: 6px 12px; + width: 100%; } .multi-file-loading-container { @@ -306,8 +307,18 @@ } .preview-container { - height: 100%; - overflow: auto; + flex-grow: 1; + position: relative; + + .md-previewer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: auto; + padding: $gl-padding; + } .file-container { background-color: $gray-darker; @@ -347,10 +358,6 @@ color: $diff-image-info-color; } } - - .md-previewer { - padding: $gl-padding; - } } .ide-mode-tabs { @@ -501,7 +508,7 @@ align-items: center; margin-bottom: 0; border-bottom: 1px solid $white-dark; - padding: $gl-btn-padding $gl-padding; + padding: 12px 0; } .multi-file-commit-panel-header-title { @@ -523,32 +530,31 @@ .multi-file-commit-list { flex: 1; overflow: auto; - padding: $gl-padding; + padding: $grid-size 0; + margin-left: -$grid-size; + margin-right: -$grid-size; min-height: 60px; + + .multi-file-commit-list-item { + margin-left: 0; + margin-right: 0; + } + + &.help-block { + margin-left: 0; + right: 0; + } } .multi-file-commit-list-item { - display: flex; - padding: 0; - align-items: center; - border-radius: $border-radius-default; - .multi-file-discard-btn { display: none; margin-top: -2px; margin-left: auto; - margin-right: $grid-size; color: $gl-link-color; - - &:focus, - &:hover { - text-decoration: underline; - } } &:hover { - background: $white-normal; - .multi-file-discard-btn { display: flex; } @@ -584,25 +590,39 @@ } } +.multi-file-commit-list-item, +.ide-file-list .file { + display: flex; + align-items: center; + margin-left: -$grid-size; + margin-right: -$grid-size; + padding: $grid-size / 2 $grid-size; + border-radius: $border-radius-default; + text-align: left; + + &:hover, + &:focus { + background: $white-normal; + } +} + .multi-file-commit-list-path { - padding: $grid-size / 2; - padding-left: $grid-size; + padding: 0; background: none; border: 0; text-align: left; width: 100%; - min-width: 0; + + &:hover, + &:focus { + outline: 0; + } svg { min-width: 16px; vertical-align: middle; display: inline-block; } - - &:hover, - &:focus { - outline: 0; - } } .multi-file-commit-list-file-path { @@ -619,12 +639,18 @@ .multi-file-commit-form { position: relative; - padding: $gl-padding; background-color: $white-light; - border-top: 1px solid $white-dark; border-left: 1px solid $white-dark; transition: all 0.3s ease; + > form, + > .commit-form-compact { + padding: $gl-padding 0; + margin-left: $gl-padding; + margin-right: $gl-padding; + border-top: 1px solid $white-dark; + } + .btn { font-size: $gl-font-size; } @@ -787,8 +813,9 @@ display: flex; flex: 1; flex-direction: column; - width: 100%; min-height: 140px; + margin-left: $gl-padding; + margin-right: $gl-padding; &.is-first { border-bottom: 1px solid $white-dark; @@ -979,9 +1006,8 @@ .ide-tree-header { display: flex; align-items: center; - padding: 10px 0; - margin-left: 10px; - margin-right: 10px; + margin-bottom: 8px; + padding: 12px 0; border-bottom: 1px solid $white-dark; .ide-new-btn { @@ -1012,9 +1038,9 @@ .commit-form-slide-up-enter-active, .commit-form-slide-up-leave-active { position: absolute; - top: 16px; - left: 16px; - right: 16px; + top: 0; + left: 0; + right: 0; transition: all 0.3s ease; } diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index e85cdcb8db7..d6a6bc7d4a1 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -1,4 +1,6 @@ class Admin::DashboardController < Admin::ApplicationController + include CountHelper + def index @projects = Project.order_id_desc.without_deleted.with_route.limit(10) @users = User.order_id_desc.limit(10) diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 7d7ff217e5d..09e143c23e8 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -94,7 +94,7 @@ module Boards def serialize_as_json(resource) resource.as_json( - only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], + only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight], labels: true, issue_endpoints: true, include_full_project_path: board.group_board?, diff --git a/app/controllers/concerns/accepts_pending_invitations.rb b/app/controllers/concerns/accepts_pending_invitations.rb new file mode 100644 index 00000000000..6e8aef52b52 --- /dev/null +++ b/app/controllers/concerns/accepts_pending_invitations.rb @@ -0,0 +1,15 @@ +module AcceptsPendingInvitations + extend ActiveSupport::Concern + + def accept_pending_invitations + return unless resource.active_for_authentication? + + clear_stored_location_for_resource if resource.accept_pending_invitations!.any? + end + + def clear_stored_location_for_resource + session_key = stored_location_key_for(resource) + + session.delete(session_key) + end +end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 6d9c38d9581..7bc46a6ccc0 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -1,4 +1,6 @@ class ConfirmationsController < Devise::ConfirmationsController + include AcceptsPendingInvitations + def almost_there flash[:notice] = nil render layout: "devise_empty" @@ -11,6 +13,8 @@ class ConfirmationsController < Devise::ConfirmationsController end def after_confirmation_path_for(resource_name, resource) + accept_pending_invitations + # incoming resource can either be a :user or an :email if signed_in?(:user) after_sign_in(resource) diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index f0e5d2aa94e..12a6cd11f80 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -23,7 +23,7 @@ class Profiles::KeysController < Profiles::ApplicationController def destroy @key = current_user.keys.find(params[:id]) - @key.destroy + Keys::DestroyService.new(current_user).execute(@key) respond_to do |format| format.html { redirect_to profile_keys_url, status: 302 } diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index b7f548e0e63..1d1184d46d1 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -23,8 +23,12 @@ class Projects::CommitController < Projects::ApplicationController respond_to do |format| format.html { render } - format.diff { render text: @commit.to_diff } - format.patch { render text: @commit.to_patch } + format.diff do + send_git_diff(@project.repository, @commit.diff_refs) + end + format.patch do + send_git_patch(@project.repository, @commit.diff_refs) + end end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index f7417a6a5aa..6b40fc2fe68 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -18,19 +18,12 @@ class Projects::PipelinesController < Projects::ApplicationController .page(params[:page]) .per(30) - @running_count = PipelinesFinder - .new(project, scope: 'running').execute.count + @running_count = limited_pipelines_count(project, 'running') + @pending_count = limited_pipelines_count(project, 'pending') + @finished_count = limited_pipelines_count(project, 'finished') + @pipelines_count = limited_pipelines_count(project) - @pending_count = PipelinesFinder - .new(project, scope: 'pending').execute.count - - @finished_count = PipelinesFinder - .new(project, scope: 'finished').execute.count - - @pipelines_count = PipelinesFinder - .new(project).execute.count - - @pipelines.map(&:commit) # List commits for batch loading + Gitlab::Ci::Pipeline::Preloader.preload(@pipelines) respond_to do |format| format.html @@ -41,7 +34,7 @@ class Projects::PipelinesController < Projects::ApplicationController pipelines: PipelineSerializer .new(project: @project, current_user: @current_user) .with_pagination(request, response) - .represent(@pipelines), + .represent(@pipelines, disable_coverage: true), count: { all: @pipelines_count, running: @running_count, @@ -185,4 +178,10 @@ class Projects::PipelinesController < Projects::ApplicationController def authorize_update_pipeline! return access_denied! unless can?(current_user, :update_pipeline, @pipeline) end + + def limited_pipelines_count(project, scope = nil) + finder = PipelinesFinder.new(project, scope: scope) + + view_context.limited_counter_with_delimiter(finder.execute) + end end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 177c8a54099..1d850baf012 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -69,7 +69,7 @@ module Projects @project_runners = @project.runners.ordered @assignable_runners = current_user - .ci_authorized_runners + .ci_owned_runners .assignable_for(project) .ordered .page(params[:page]).per(20) diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb index 1ff08cce8cb..d9fecfecc40 100644 --- a/app/controllers/projects/settings/integrations_controller.rb +++ b/app/controllers/projects/settings/integrations_controller.rb @@ -11,7 +11,14 @@ module Projects @hook = ProjectHook.new # Services - @services = @project.find_or_initialize_services + @services = @project.find_or_initialize_services(exceptions: service_exceptions) + end + + private + + # Returns a list of services that should be hidden from the list + def service_exceptions + @project.disabled_services.dup end end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 1848c806c41..f5a222b3a48 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,5 +1,6 @@ class RegistrationsController < Devise::RegistrationsController include Recaptcha::Verify + include AcceptsPendingInvitations before_action :whitelist_query_limiting, only: [:destroy] @@ -16,6 +17,7 @@ class RegistrationsController < Devise::RegistrationsController end if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha + accept_pending_invitations super else flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' @@ -60,7 +62,7 @@ class RegistrationsController < Devise::RegistrationsController def after_sign_up_path_for(user) Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}") - user.confirmed? ? dashboard_projects_path : users_almost_there_path + user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path end def after_inactive_sign_up_path_for(resource) diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb new file mode 100644 index 00000000000..24ee62e68ba --- /dev/null +++ b/app/helpers/count_helper.rb @@ -0,0 +1,5 @@ +module CountHelper + def approximate_count_with_delimiters(model) + number_with_delimiter(Gitlab::Database::Count.approximate_count(model)) + end +end diff --git a/app/models/appearance.rb b/app/models/appearance.rb index fb66dd0b766..f8713138a93 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -2,6 +2,7 @@ class Appearance < ActiveRecord::Base include CacheMarkdownField include AfterCommitQueue include ObjectStorage::BackgroundMove + include WithUploads cache_markdown_field :description cache_markdown_field :new_project_guidelines @@ -14,8 +15,6 @@ class Appearance < ActiveRecord::Base mount_uploader :logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader - has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - CACHE_KEY = "current_appearance:#{Gitlab::VERSION}".freeze after_commit :flush_redis_cache diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 61c10c427dd..d9649e30edc 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -184,7 +184,7 @@ module Ci end def playable? - action? && (manual? || complete?) + action? && (manual? || retryable?) end def action? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 1f49764e7cc..c26f0b6dcdc 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -406,7 +406,18 @@ module Ci end def has_warnings? - builds.latest.failed_but_allowed.any? + number_of_warnings.positive? + end + + def number_of_warnings + BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader| + Build.where(commit_id: pipeline_ids) + .latest + .failed_but_allowed + .group(:commit_id) + .count + .each { |id, amount| loader.call(id, amount) } + end end def set_config_source diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index bda69f85a78..e6f1ed519be 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -52,7 +52,7 @@ module Ci # Without that, placeholders would miss one and couldn't match. where(locked: false) .where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})") - .specific + .project_type end validate :tag_constraints diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 7b25d8c4089..c702c4ee807 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -49,6 +49,11 @@ module Clusters # ensures headers containing auth data are appended to original k8s client options options = kube_client.rest_client.options.merge(headers: kube_client.headers) RestClient::Resource.new(proxy_url, options) + rescue Kubeclient::HttpError + # If users have mistakenly set parameters or removed the depended clusters, + # `proxy_url` could raise an exception because gitlab can not communicate with the cluster. + # Since `PrometheusAdapter#can_query?` is eargely loaded on environement pages in gitlab, + # we need to silence the exceptions end private diff --git a/app/models/commit.rb b/app/models/commit.rb index b46f9f34689..56d4c86774e 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -224,8 +224,34 @@ class Commit Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message) end + def lazy_author + BatchLoader.for(author_email.downcase).batch do |emails, loader| + # A Hash that maps user Emails to the corresponding User objects. The + # Emails at this point are the _primary_ Emails of the Users. + users_for_emails = User + .by_any_email(emails) + .each_with_object({}) { |user, hash| hash[user.email] = user } + + users_for_ids = users_for_emails + .values + .each_with_object({}) { |user, hash| hash[user.id] = user } + + # Some commits may have used an alternative Email address. In this case we + # need to query the "emails" table to map those addresses to User objects. + Email + .where(email: emails - users_for_emails.keys) + .pluck(:email, :user_id) + .each { |(email, id)| users_for_emails[email] = users_for_ids[id] } + + users_for_emails.each { |email, user| loader.call(email, user) } + end + end + def author - User.find_by_any_email(author_email.downcase) + # We use __sync so that we get the actual objects back (including an actual + # nil), instead of a wrapper, as returning a wrapped nil breaks a lot of + # code. + lazy_author.__sync end request_cache(:author) { author_email.downcase } diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 97d89422594..a7d05722287 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base include HasStatus include Importable include AfterCommitQueue + include Presentable self.table_name = 'ci_builds' diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index cefa5c13c5f..db7254c27e0 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -12,8 +12,8 @@ module Sortable scope :order_created_asc, -> { reorder(created_at: :asc) } scope :order_updated_desc, -> { reorder(updated_at: :desc) } scope :order_updated_asc, -> { reorder(updated_at: :asc) } - scope :order_name_asc, -> { reorder(name: :asc) } - scope :order_name_desc, -> { reorder(name: :desc) } + scope :order_name_asc, -> { reorder("lower(name) asc") } + scope :order_name_desc, -> { reorder("lower(name) desc") } end module ClassMethods diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index 73fc5048dcf..1caf47072bc 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -53,6 +53,10 @@ module TimeTrackable Gitlab::TimeTrackingFormatter.output(time_estimate) end + def time_estimate=(val) + val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val) + end + private def touchable? diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb new file mode 100644 index 00000000000..e7cfffb775b --- /dev/null +++ b/app/models/concerns/with_uploads.rb @@ -0,0 +1,39 @@ +# Mounted uploaders are destroyed by carrierwave's after_commit +# hook. This hook fetches upload location (local vs remote) from +# Upload model. So it's neccessary to make sure that during that +# after_commit hook model's associated uploads are not deleted yet. +# IOW we can not use dependent: :destroy : +# has_many :uploads, as: :model, dependent: :destroy +# +# And because not-mounted uploads require presence of upload's +# object model when destroying them (FileUploader's `build_upload` method +# references `model` on delete), we can not use after_commit hook for these +# uploads. +# +# Instead FileUploads are destroyed in before_destroy hook and remaining uploads +# are destroyed by the carrierwave's after_commit hook. + +module WithUploads + extend ActiveSupport::Concern + + # Currently there is no simple way how to select only not-mounted + # uploads, it should be all FileUploaders so we select them by + # `uploader` class + FILE_UPLOADERS = %w(PersonalFileUploader NamespaceFileUploader FileUploader).freeze + + included do + has_many :uploads, as: :model + + before_destroy :destroy_file_uploads + end + + # mounted uploads are deleted in carrierwave's after_commit hook, + # but FileUploaders which are not mounted must be deleted explicitly and + # it can not be done in after_commit because FileUploader requires loads + # associated model on destroy (which is already deleted in after_commit) + def destroy_file_uploads + self.uploads.where(uploader: FILE_UPLOADERS).find_each do |upload| + upload.destroy + end + end +end diff --git a/app/models/group.rb b/app/models/group.rb index cefca316399..8fb77a7869d 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -10,6 +10,7 @@ class Group < Namespace include LoadedInGroupList include GroupDescendant include TokenAuthenticatable + include WithUploads has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent alias_method :members, :group_members @@ -30,8 +31,6 @@ class Group < Namespace has_many :variables, class_name: 'Ci::GroupVariable' has_many :custom_attributes, class_name: 'GroupCustomAttribute' - has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_many :boards has_many :badges, class_name: 'GroupBadge' diff --git a/app/models/list.rb b/app/models/list.rb index 918275be142..5daf35ef845 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -31,7 +31,8 @@ class List < ActiveRecord::Base if options.key?(:label) json[:label] = label.as_json( project: board.project, - only: [:id, :title, :description, :color] + only: [:id, :title, :description, :color], + methods: [:text_color] ) end end diff --git a/app/models/project.rb b/app/models/project.rb index 534a0e630af..35c873830a7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -23,6 +23,7 @@ class Project < ActiveRecord::Base include ::Gitlab::Utils::StrongMemoize include ChronicDurationAttribute include FastDestroyAll::Helpers + include WithUploads extend Gitlab::ConfigHelper @@ -301,8 +302,6 @@ class Project < ActiveRecord::Base inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } validates :variables, variable_duplicates: { scope: :environment_scope } - has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - # Scopes scope :pending_delete, -> { where(pending_delete: true) } scope :without_deleted, -> { where(pending_delete: false) } @@ -998,7 +997,7 @@ class Project < ActiveRecord::Base available_services_names = Service.available_services_names - exceptions - available_services_names.map do |service_name| + available_services = available_services_names.map do |service_name| service = find_service(services, service_name) if service @@ -1015,6 +1014,14 @@ class Project < ActiveRecord::Base end end end + + available_services.reject do |service| + disabled_services.include?(service.to_param) + end + end + + def disabled_services + [] end def find_or_initialize_service(name) diff --git a/app/models/user.rb b/app/models/user.rb index 173ab38e20c..8ef3c3ceff0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -17,6 +17,7 @@ class User < ActiveRecord::Base include IgnorableColumn include BulkMemberAccessLoad include BlocksJsonSerialization + include WithUploads DEFAULT_NOTIFICATION_LEVEL = :participating @@ -137,7 +138,6 @@ class User < ActiveRecord::Base has_many :custom_attributes, class_name: 'UserCustomAttribute' has_many :callouts, class_name: 'UserCallout' - has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :term_agreements belongs_to :accepted_term, class_name: 'ApplicationSetting::Term' @@ -860,6 +860,16 @@ class User < ActiveRecord::Base confirmed? && !temp_oauth_email? end + def accept_pending_invitations! + pending_invitations.select do |member| + member.accept_invite!(self) + end + end + + def pending_invitations + Member.where(invite_email: verified_emails).invite + end + def all_emails all_emails = [] all_emails << email unless temp_oauth_email? @@ -999,12 +1009,19 @@ class User < ActiveRecord::Base !solo_owned_groups.present? end - def ci_authorized_runners - @ci_authorized_runners ||= begin - runner_ids = Ci::RunnerProject + def ci_owned_runners + @ci_owned_runners ||= begin + project_runner_ids = Ci::RunnerProject .where(project: authorized_projects(Gitlab::Access::MASTER)) .select(:runner_id) - Ci::Runner.specific.where(id: runner_ids) + + group_runner_ids = Ci::RunnerNamespace + .where(namespace_id: owned_or_masters_groups.select(:id)) + .select(:runner_id) + + union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids]) + + Ci::Runner.specific.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection end end @@ -1205,6 +1222,11 @@ class User < ActiveRecord::Base !terms_accepted? end + def owned_or_masters_groups + union = Gitlab::SQL::Union.new([owned_groups, masters_groups]) + Group.from("(#{union.to_sql}) namespaces") + end + protected # override, from Devise::Validatable diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb index 7dff8470e23..895abe87d86 100644 --- a/app/policies/ci/runner_policy.rb +++ b/app/policies/ci/runner_policy.rb @@ -1,16 +1,19 @@ module Ci class RunnerPolicy < BasePolicy with_options scope: :subject, score: 0 - condition(:shared) { @subject.is_shared? } - - with_options scope: :subject, score: 0 condition(:locked, scope: :subject) { @subject.locked? } - condition(:authorized_runner) { @user.ci_authorized_runners.include?(@subject) } + condition(:owned_runner) { @user.ci_owned_runners.exists?(@subject.id) } rule { anonymous }.prevent_all - rule { admin | authorized_runner }.enable :assign_runner - rule { ~admin & shared }.prevent :assign_runner + + rule { admin | owned_runner }.policy do + enable :assign_runner + enable :read_runner + enable :update_runner + enable :delete_runner + end + rule { ~admin & locked }.prevent :assign_runner end end diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb index 4873d7ce662..e0aaa5cb736 100644 --- a/app/presenters/ci/build_presenter.rb +++ b/app/presenters/ci/build_presenter.rb @@ -1,16 +1,5 @@ module Ci - class BuildPresenter < Gitlab::View::Presenter::Delegated - CALLOUT_FAILURE_MESSAGES = { - unknown_failure: 'There is an unknown failure, please try again', - script_failure: 'There has been a script failure. Check the job log for more information', - api_failure: 'There has been an API failure, please try again', - stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again', - runner_system_failure: 'There has been a runner system failure, please try again', - missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information' - }.freeze - - presents :build - + class BuildPresenter < CommitStatusPresenter def erased_by_user? # Build can be erased through API, therefore it does not have # `erased_by` user assigned in that case. @@ -44,14 +33,6 @@ module Ci "#{subject.name} - #{detailed_status.status_tooltip}" end - def callout_failure_message - CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym] - end - - def recoverable? - failed? && !unrecoverable? - end - private def tooltip_for_badge @@ -61,9 +42,5 @@ module Ci def detailed_status @detailed_status ||= subject.detailed_status(user) end - - def unrecoverable? - script_failure? || missing_dependency_failure? - end end end diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb new file mode 100644 index 00000000000..c7f7aa836bd --- /dev/null +++ b/app/presenters/commit_status_presenter.rb @@ -0,0 +1,24 @@ +class CommitStatusPresenter < Gitlab::View::Presenter::Delegated + CALLOUT_FAILURE_MESSAGES = { + unknown_failure: 'There is an unknown failure, please try again', + script_failure: 'There has been a script failure. Check the job log for more information', + api_failure: 'There has been an API failure, please try again', + stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again', + runner_system_failure: 'There has been a runner system failure, please try again', + missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information' + }.freeze + + presents :build + + def callout_failure_message + CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym] + end + + def recoverable? + failed? && !unrecoverable? + end + + def unrecoverable? + script_failure? || missing_dependency_failure? + end +end diff --git a/app/presenters/generic_commit_status_presenter.rb b/app/presenters/generic_commit_status_presenter.rb new file mode 100644 index 00000000000..da09df29a37 --- /dev/null +++ b/app/presenters/generic_commit_status_presenter.rb @@ -0,0 +1,2 @@ +class GenericCommitStatusPresenter < CommitStatusPresenter +end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index 6457294b285..f782b411b84 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -4,7 +4,11 @@ class PipelineEntity < Grape::Entity expose :id expose :user, using: UserEntity expose :active?, as: :active - expose :coverage + + # Coverage isn't always necessary (e.g. when displaying project pipelines in + # the UI). Instead of creating an entirely different entity we just allow the + # disabling of this specific field whenever necessary. + expose :coverage, unless: proc { options[:disable_coverage] } expose :source expose :created_at, :updated_at diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb index f78791932a7..df8e82f5f60 100644 --- a/app/services/keys/base_service.rb +++ b/app/services/keys/base_service.rb @@ -2,7 +2,7 @@ module Keys class BaseService attr_accessor :user, :params - def initialize(user, params) + def initialize(user, params = {}) @user, @params = user, params @ip_address = @params.delete(:ip_address) end diff --git a/app/services/keys/destroy_service.rb b/app/services/keys/destroy_service.rb new file mode 100644 index 00000000000..785cfa3a1d8 --- /dev/null +++ b/app/services/keys/destroy_service.rb @@ -0,0 +1,12 @@ +module Keys + class DestroyService < ::Keys::BaseService + def execute(key) + key.destroy if destroy_possible?(key) + end + + # overriden in EE::Keys::DestroyService + def destroy_possible?(key) + true + end + end +end diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb index 6c93dc69bb0..7eb89339a92 100644 --- a/app/services/lfs/unlock_file_service.rb +++ b/app/services/lfs/unlock_file_service.rb @@ -2,14 +2,14 @@ module Lfs class UnlockFileService < BaseService def execute unless can?(current_user, :push_code, project) - raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions' + raise Gitlab::GitAccess::UnauthorizedError, _('You have no permissions') end unlock_file rescue Gitlab::GitAccess::UnauthorizedError => ex error(ex.message, 403) rescue ActiveRecord::RecordNotFound - error('Lock not found', 404) + error(_('Lock not found'), 404) rescue => ex error(ex.message, 500) end @@ -24,9 +24,9 @@ module Lfs success(lock: lock, http_status: :ok) elsif forced - error('You must have master access to force delete a lock', 403) + error(_('You must have master access to force delete a lock'), 403) else - error("#{lock.path} is locked by GitLab User #{lock.user_id}", 403) + error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403) end end diff --git a/app/services/milestones/base_service.rb b/app/services/milestones/base_service.rb index 4963601ea8b..cce0863d611 100644 --- a/app/services/milestones/base_service.rb +++ b/app/services/milestones/base_service.rb @@ -5,6 +5,7 @@ module Milestones def initialize(parent, user, params = {}) @parent, @current_user, @params = parent, user, params.dup + super end end end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index bbf0e0fb95c..41ef646fc0e 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -10,7 +10,7 @@ = link_to admin_projects_path do %h3.text-center Projects: - = number_with_delimiter(Project.cached_count) + = approximate_count_with_delimiters(Project) %hr = link_to('New project', new_project_path, class: "btn btn-new") .col-sm-4 @@ -19,7 +19,7 @@ = link_to admin_users_path do %h3.text-center Users: - = number_with_delimiter(User.count) + = approximate_count_with_delimiters(User) %hr = link_to 'New user', new_admin_user_path, class: "btn btn-new" .col-sm-4 @@ -28,7 +28,7 @@ = link_to admin_groups_path do %h3.text-center Groups: - = number_with_delimiter(Group.count) + = approximate_count_with_delimiters(Group) %hr = link_to 'New group', new_admin_group_path, class: "btn btn-new" .row @@ -39,31 +39,31 @@ %p Forks %span.light.pull-right - = number_with_delimiter(ForkedProjectLink.count) + = approximate_count_with_delimiters(ForkedProjectLink) %p Issues %span.light.pull-right - = number_with_delimiter(Issue.count) + = approximate_count_with_delimiters(Issue) %p Merge Requests %span.light.pull-right - = number_with_delimiter(MergeRequest.count) + = approximate_count_with_delimiters(MergeRequest) %p Notes %span.light.pull-right - = number_with_delimiter(Note.count) + = approximate_count_with_delimiters(Note) %p Snippets %span.light.pull-right - = number_with_delimiter(Snippet.count) + = approximate_count_with_delimiters(Snippet) %p SSH Keys %span.light.pull-right - = number_with_delimiter(Key.count) + = approximate_count_with_delimiters(Key) %p Milestones %span.light.pull-right - = number_with_delimiter(Milestone.count) + = approximate_count_with_delimiters(Milestone) %p Active Users %span.light.pull-right diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 1c5b4aecabb..2244d16f0a6 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -121,7 +121,7 @@ %tr %td.shortcut .key g - .key e + .key v %td Go to the project's activity feed %tr @@ -175,6 +175,18 @@ %tr %td.shortcut .key g + .key e + %td + Go to environments + %tr + %td.shortcut + .key g + .key k + %td + Go to kubernetes + %tr + %td.shortcut + .key g .key s %td Go to snippets diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index c3ea592a6b5..1cdb57bdfe8 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -154,7 +154,7 @@ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts]) do = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do .nav-icon-container - = sprite_icon('pipeline') + = sprite_icon('rocket') %span.nav-item-name = _('CI / CD') @@ -212,7 +212,7 @@ - if project_nav_tab? :clusters - show_cluster_hint = show_gke_cluster_integration_callout?(@project) = nav_link(controller: [:clusters, :user, :gcp]) do - = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-cluster' do + = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do %span = _('Kubernetes') - if show_cluster_hint diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml index b8b75da3f2f..6730172242b 100644 --- a/app/views/notify/member_invited_email.html.haml +++ b/app/views/notify/member_invited_email.html.haml @@ -4,7 +4,7 @@ by = link_to member.created_by.name, user_url(member.created_by) to join the - = link_to member_source.human_name, member_source.web_url + = link_to member_source.human_name, member_source.public? ? member_source.web_url : invite_url(@token) #{member_source.model_name.singular} as #{member.human_access}. %p diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index a603b1024eb..a603b1024eb 100755..100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index 4bff6468bb0..aea40df41b0 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -15,7 +15,7 @@ ":title" => '(list.label ? list.label.description : "")', data: { container: "body", placement: "bottom" }, class: "label color-label title board-title-text", - ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.text_color ? list.label.text_color : \"#2e2e2e\") }" } + ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" } {{ list.title }} - if can?(current_user, :admin_list, current_board_parent) diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index 893a7f26ebd..d4e67b5e7e3 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -41,8 +41,9 @@ - if note.system %span.system-note-message = markdown_field(note, :note) - %a{ href: "##{dom_id(note)}" } - = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') + %span.system-note-separator + · + %a.system-note-separator{ href: "##{dom_id(note)}" }= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - unless note.system? .note-actions - if note.for_personal_snippet? diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 836230ae8ee..9f55c10d19b 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -45,6 +45,6 @@ %strong.embed-toggle-list-item= _("Share") %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed } .input-group-btn - %button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' } + %button.js-clipboard-btn.snippet-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '.js-snippet-url-area' } = sprite_icon('duplicate', size: 16) .clearfix diff --git a/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml b/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml new file mode 100644 index 00000000000..9287243a7e3 --- /dev/null +++ b/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml @@ -0,0 +1,5 @@ +--- +title: Fix double-brackets being linkified in wiki markdown +merge_request: 18524 +author: brewingcode +type: fixed diff --git a/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml new file mode 100644 index 00000000000..a97e8a2b5cc --- /dev/null +++ b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml @@ -0,0 +1,5 @@ +--- +title: Add API endpoint to render markdown text +merge_request: 18926 +author: "@blackst0ne" +type: added diff --git a/changelogs/unreleased/22647-width-contributors-graphs.yml b/changelogs/unreleased/22647-width-contributors-graphs.yml new file mode 100644 index 00000000000..87be3a25d8a --- /dev/null +++ b/changelogs/unreleased/22647-width-contributors-graphs.yml @@ -0,0 +1,5 @@ +--- +title: Fix width of contributors graphs +merge_request: 18639 +author: Paul Vorbach +type: fixed diff --git a/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml new file mode 100644 index 00000000000..9f07fcdfa0b --- /dev/null +++ b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml @@ -0,0 +1,5 @@ +--- +title: Apply NestingDepth (level 5) (pages/pipelines.scss) +merge_request: 18830 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/42531-open-invite-404.yml b/changelogs/unreleased/42531-open-invite-404.yml new file mode 100644 index 00000000000..73729f4a929 --- /dev/null +++ b/changelogs/unreleased/42531-open-invite-404.yml @@ -0,0 +1,5 @@ +--- +title: Automatically accepts project/group invite by email after user signup +merge_request: 17634 +author: Jacopo Beschi @jacopo-beschi +type: changed diff --git a/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml new file mode 100644 index 00000000000..b9e70bc5679 --- /dev/null +++ b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml @@ -0,0 +1,5 @@ +--- +title: Fix unscrollable Markdown preview of WebIDE on Firefox +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/46177-fix-present-on-generic-commit-status.yml b/changelogs/unreleased/46177-fix-present-on-generic-commit-status.yml new file mode 100644 index 00000000000..2f885c5c927 --- /dev/null +++ b/changelogs/unreleased/46177-fix-present-on-generic-commit-status.yml @@ -0,0 +1,5 @@ +--- +title: Allow CommitStatus class to use presentable methods +merge_request: 18979 +author: +type: fixed diff --git a/changelogs/unreleased/46193-fix-big-estimate.yml b/changelogs/unreleased/46193-fix-big-estimate.yml new file mode 100644 index 00000000000..d0da0c10033 --- /dev/null +++ b/changelogs/unreleased/46193-fix-big-estimate.yml @@ -0,0 +1,5 @@ +--- +title: Fixes 500 error on /estimate BIG_VALUE +merge_request: 18964 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/changelogs/unreleased/46303_copy_button_fix_embedded_snippets.yml b/changelogs/unreleased/46303_copy_button_fix_embedded_snippets.yml new file mode 100644 index 00000000000..c8cdf3672b3 --- /dev/null +++ b/changelogs/unreleased/46303_copy_button_fix_embedded_snippets.yml @@ -0,0 +1,5 @@ +--- +title: fixed copy to blipboard button in embed bar of snippets +merge_request: 18923 +author: haseebeqx +type: fixed diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml new file mode 100644 index 00000000000..609968f3230 --- /dev/null +++ b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml @@ -0,0 +1,5 @@ +--- +title: Adds keyboard shortcut `g e` for Environments on Project pages +merge_request: 19002 +author: +type: added diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml new file mode 100644 index 00000000000..48e51b2615e --- /dev/null +++ b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml @@ -0,0 +1,5 @@ +--- +title: Adds keyboard shortcut `g k` for Kubernetes on Project pages +merge_request: 19002 +author: +type: added diff --git a/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml new file mode 100644 index 00000000000..9a7cf0d6944 --- /dev/null +++ b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml @@ -0,0 +1,5 @@ +--- +title: Changes keyboard shortcut of Activity feed to `g v` +merge_request: 19002 +author: +type: changed diff --git a/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml new file mode 100644 index 00000000000..f416e35030e --- /dev/null +++ b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml @@ -0,0 +1,5 @@ +--- +title: Removes outdated `g t` shortcut for TODO in favor of `Shift+T` +merge_request: 19002 +author: +type: removed diff --git a/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml new file mode 100644 index 00000000000..a0d787e570e --- /dev/null +++ b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml @@ -0,0 +1,3 @@ +title: Add anchor for incoming email regex +merge_request: !18917 +type: added diff --git a/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml new file mode 100644 index 00000000000..d77c5b42497 --- /dev/null +++ b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml @@ -0,0 +1,5 @@ +--- +title: Add support for variables expression pattern matching syntax +merge_request: 18902 +author: +type: added diff --git a/changelogs/unreleased/fix-assignee-name-wrap.yml b/changelogs/unreleased/fix-assignee-name-wrap.yml new file mode 100644 index 00000000000..2407288785f --- /dev/null +++ b/changelogs/unreleased/fix-assignee-name-wrap.yml @@ -0,0 +1,5 @@ +--- +title: Wrapping problem on the issues page has been fixed +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml new file mode 100644 index 00000000000..c2a788d6ad0 --- /dev/null +++ b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml @@ -0,0 +1,5 @@ +--- +title: Do not allow to trigger manual actions that were skipped +merge_request: 18985 +author: +type: fixed diff --git a/changelogs/unreleased/fix-kube_client-proxy_url-exception.yml b/changelogs/unreleased/fix-kube_client-proxy_url-exception.yml new file mode 100644 index 00000000000..1f64ab9f30f --- /dev/null +++ b/changelogs/unreleased/fix-kube_client-proxy_url-exception.yml @@ -0,0 +1,5 @@ +--- +title: Fix corrupted environment pages with unathorized proxy url +merge_request: 18989 +author: +type: fixed diff --git a/changelogs/unreleased/jivl-add-dot-system-notes.yml b/changelogs/unreleased/jivl-add-dot-system-notes.yml new file mode 100644 index 00000000000..2246bab1464 --- /dev/null +++ b/changelogs/unreleased/jivl-add-dot-system-notes.yml @@ -0,0 +1,5 @@ +--- +title: Add dot to separate system notes content +merge_request: 18864 +author: +type: changed diff --git a/changelogs/unreleased/jprovazn-remote-upload-destroy.yml b/changelogs/unreleased/jprovazn-remote-upload-destroy.yml new file mode 100644 index 00000000000..22e55920fa3 --- /dev/null +++ b/changelogs/unreleased/jprovazn-remote-upload-destroy.yml @@ -0,0 +1,5 @@ +--- +title: Fix deletion of Object Store uploads +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/memoize-database-version.yml b/changelogs/unreleased/memoize-database-version.yml new file mode 100644 index 00000000000..575348a53a1 --- /dev/null +++ b/changelogs/unreleased/memoize-database-version.yml @@ -0,0 +1,5 @@ +--- +title: Memoize Gitlab::Database.version +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/pipelines-index-performance.yml b/changelogs/unreleased/pipelines-index-performance.yml new file mode 100644 index 00000000000..928c2ddab72 --- /dev/null +++ b/changelogs/unreleased/pipelines-index-performance.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of project pipelines pages +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/sh-fix-grape-logging-status-code.yml b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml new file mode 100644 index 00000000000..aabf9a84bfb --- /dev/null +++ b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml @@ -0,0 +1,5 @@ +--- +title: Fix api_json.log not always reporting the right HTTP status code +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-move-delete-groups-api-async.yml b/changelogs/unreleased/sh-move-delete-groups-api-async.yml new file mode 100644 index 00000000000..1b200cac5c5 --- /dev/null +++ b/changelogs/unreleased/sh-move-delete-groups-api-async.yml @@ -0,0 +1,5 @@ +--- +title: Move API group deletion to Sidekiq +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml new file mode 100644 index 00000000000..098e4b1d5fa --- /dev/null +++ b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml @@ -0,0 +1,5 @@ +--- +title: "Use case in-sensitive ordering by name for dashboard" +merge_request: 18553 +author: "@vedharish" +type: fixed diff --git a/changelogs/unreleased/xeodon-gitlab-ce-fix-45743-master-fix-gitaly-delete-refs.yml b/changelogs/unreleased/xeodon-gitlab-ce-fix-45743-master-fix-gitaly-delete-refs.yml new file mode 100644 index 00000000000..94da4d74300 --- /dev/null +++ b/changelogs/unreleased/xeodon-gitlab-ce-fix-45743-master-fix-gitaly-delete-refs.yml @@ -0,0 +1,5 @@ +--- +title: Fix error when deleting an empty list of refs +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/zj-calculate-checksum-mandator.yml b/changelogs/unreleased/zj-calculate-checksum-mandator.yml new file mode 100644 index 00000000000..83315a3c5dd --- /dev/null +++ b/changelogs/unreleased/zj-calculate-checksum-mandator.yml @@ -0,0 +1,5 @@ +--- +title: Remove shellout implementation for Repository checksums +merge_request: +author: +type: other diff --git a/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml b/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml new file mode 100644 index 00000000000..bce68692d98 --- /dev/null +++ b/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml @@ -0,0 +1,5 @@ +--- +title: Workhorse to send raw diff and patch for commits +merge_request: +author: +type: other diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb index c8d7f742bb1..14616e726d9 100644 --- a/config/initializers/deprecations.rb +++ b/config/initializers/deprecations.rb @@ -1,4 +1,4 @@ -if Gitlab.dev_env_or_com? +if Rails.env.development? || ENV['GITLAB_LEGACY_PATH_LOG_MESSAGE'] deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab') deprecator.behavior = -> (message, callstack) { diff --git a/db/migrate/20180504195842_project_name_lower_index.rb b/db/migrate/20180504195842_project_name_lower_index.rb new file mode 100644 index 00000000000..d6f25d3d4ab --- /dev/null +++ b/db/migrate/20180504195842_project_name_lower_index.rb @@ -0,0 +1,32 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ProjectNameLowerIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + INDEX_NAME = 'index_projects_on_lower_name' + + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + disable_statement_timeout + + execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON projects (LOWER(name))" + end + + def down + return unless Gitlab::Database.postgresql? + + disable_statement_timeout + + if supports_drop_index_concurrently? + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}" + else + execute "DROP INDEX IF EXISTS #{INDEX_NAME}" + end + end +end diff --git a/doc/api/README.md b/doc/api/README.md index e777fc63d2b..194907accc7 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -33,6 +33,7 @@ following locations: - [Jobs](jobs.md) - [Keys](keys.md) - [Labels](labels.md) +- [Markdown](markdown.md) - [Merge Requests](merge_requests.md) - [Project milestones](milestones.md) - [Group milestones](group_milestones.md) diff --git a/doc/api/groups.md b/doc/api/groups.md index 923fd662a5b..96842ef330f 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -487,6 +487,9 @@ Parameters: - `id` (required) - The ID or path of a user group +This will queue a background job to delete all projects in the group. The +response will be a 202 Accepted if the user has authorization. + ## Search for group Get all groups that match your string in their name or path. diff --git a/doc/api/markdown.md b/doc/api/markdown.md new file mode 100644 index 00000000000..f406838e887 --- /dev/null +++ b/doc/api/markdown.md @@ -0,0 +1,29 @@ +# Markdown API + +> [Introduced][ce-18926] in GitLab 11.0. + +Available only in APIv4. + +## Render an arbitrary Markdown document + +``` +POST /api/v4/markdown +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | ------------- | ------------------------------------------ | +| `text` | string | yes | The markdown text to render | +| `gfm` | boolean | no (optional) | Render text using GitLab Flavored Markdown. Default is `false` | +| `project` | string | no (optional) | Use `project` as a context when creating references using GitLab Flavored Markdown. [Authentication](README.html#authentication) is required if a project is not public. | + +```bash +curl --header Content-Type:application/json --data '{"text":"Hello world! :tada:", "gfm":true, "project":"group_example/project_example"}' https://gitlab.example.com/api/v4/markdown +``` + +Response example: + +```json +{ "html": "<p dir=\"auto\">Hello world! <gl-emoji title=\"party popper\" data-name=\"tada\" data-unicode-version=\"6.0\">🎉</gl-emoji></p>" } +``` + +[ce-18926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18926 diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 42367bf13f7..f66b2b374ba 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -530,6 +530,16 @@ Below you can find supported syntax reference: `$STAGING` value needs to a string, with length higher than zero. Variable that contains only whitespace characters is not an empty variable. +1. Pattern matching _(added in 11.0)_ + + > Example: `$VARIABLE =~ /^content.*/` + + It is possible perform pattern matching against a variable and regular + expression. Expression like this evaluates to truth if matches are found. + + Pattern matching is case-sensitive by default. Use `i` flag modifier, like + `/pattern/i` to make a pattern case-insensitive. + ### Unsupported predefined variables Because GitLab evaluates variables before creating jobs, we do not support a diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 2a17a51d7f8..3e77a6f58b7 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -344,10 +344,11 @@ job: kubernetes: active ``` -Example of using variables expressions: +Examples of using variables expressions: ```yaml deploy: + script: cap staging deploy only: refs: - branches @@ -356,6 +357,16 @@ deploy: - $STAGING ``` +Another use case is exluding jobs depending on a commit message _(added in 11.0)_: + +```yaml +end-to-end: + script: rake test:end-to-end + except: + variables: + - $CI_COMMIT_MESSAGE =~ /skip-end-to-end-tests/ +``` + Learn more about variables expressions on [a separate page][variables-expressions]. ## `tags` diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md index 8997a5889dc..858b03c60bf 100644 --- a/doc/development/fe_guide/vuex.md +++ b/doc/development/fe_guide/vuex.md @@ -37,12 +37,13 @@ import state from './state'; Vue.use(Vuex); -export default new Vuex.Store({ +export const createStore = () => new Vuex.Store({ actions, getters, mutations, state, }); +export default createStore(); ``` ### `state.js` @@ -320,10 +321,11 @@ In order to write unit tests for those components, we need to include the store ```javascript //component_spec.js import Vue from 'vue'; -import store from './store'; +import { createStore } from './store'; import component from './component.vue' describe('component', () => { + let store; let vm; let Component; @@ -340,6 +342,8 @@ describe('component', () => { name: 'Foo', age: '30', }; + + store = createStore(); // populate the store store.dispatch('addUser', user); diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md index f656057e3da..236408762e3 100644 --- a/doc/downgrade_ee_to_ce/README.md +++ b/doc/downgrade_ee_to_ce/README.md @@ -15,9 +15,9 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so you should disable these mechanisms before downgrading and you should provide alternative authentication methods to your users. -### Remove Jenkins CI Service entries from the database +### Remove Service Integration entries from the database -The `JenkinsService` class is only available on the Enterprise Edition codebase, +The `JenkinsService` and `GithubService` classes are only available in the Enterprise Edition codebase, so if you downgrade to the Community Edition, you'll come across the following error: @@ -30,20 +30,31 @@ column if you didn't intend it to be used for storing the inheritance class or o use another column for that information.) ``` +or + +``` +Completed 500 Internal Server Error in 497ms (ActiveRecord: 32.2ms) + +ActionView::Template::Error (The single-table inheritance mechanism failed to locate the subclass: 'GithubService'. This +error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this +column if you didn't intend it to be used for storing the inheritance class or overwrite Service.inheritance_column to +use another column for that information.) +``` + All services are created automatically for every project you have, so in order to avoid getting this error, you need to remove all instances of the -`JenkinsService` from your database: +`JenkinsService` and `GithubService` from your database: **Omnibus Installation** ``` -$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" +$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" ``` **Source Installation** ``` -$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" production +$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production ``` ### Secret variables environment scopes diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 785cc32d590..77139c50d07 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -64,6 +64,13 @@ If you are running GitLab within a Docker container, you can run the backup from docker exec -t <container name> gitlab-rake gitlab:backup:create ``` +If you are using the gitlab-omnibus helm chart on a Kubernetes cluster, you can +run the backup task on the gitlab application pod using kubectl + +``` +kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:create +``` + Example output: ``` @@ -601,6 +608,34 @@ If there is a GitLab version mismatch between your backup tar file and the insta version of GitLab, the restore command will abort with an error. Install the [correct GitLab version](https://packages.gitlab.com/gitlab/) and try again. +### Restore for Docker image and gitlab-omnibus helm chart + +For GitLab installations using docker image or the gitlab-omnibus helm chart on +a Kubernetes cluster, restore task expects the restore directories to be empty. +However, with docker and Kubernetes volume mounts, some system level directories +may be created at the volume roots, like `lost+found` directory found in Linux +operating systems. These directories are usually owned by `root`, which can +cause access permission errors since the restore rake task runs as `git` user. +So, to restore a GitLab installation, users have to confirm the restore target +directories are empty. + +For both these installation types, the backup tarball has to be available in the +backup location (default location is `/var/opt/gitlab/backups`). + +For docker installations, the restore task can be run from host using the +command + +``` +docker exec -it <name of container> gitlab-rake gitlab:backup:restore +``` + +Similarly, for gitlab-omnibus helm chart, the restore task can be run on the +gitlab application pod using kubectl + +``` +kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:restore +``` + ## Alternative backup strategies If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow. diff --git a/doc/topics/autodevops/img/rollout_enabled.png b/doc/topics/autodevops/img/rollout_enabled.png Binary files differnew file mode 100644 index 00000000000..d6e7d790cf2 --- /dev/null +++ b/doc/topics/autodevops/img/rollout_enabled.png diff --git a/doc/topics/autodevops/img/rollout_staging_disabled.png b/doc/topics/autodevops/img/rollout_staging_disabled.png Binary files differnew file mode 100644 index 00000000000..71e36b440f0 --- /dev/null +++ b/doc/topics/autodevops/img/rollout_staging_disabled.png diff --git a/doc/topics/autodevops/img/rollout_staging_enabled.png b/doc/topics/autodevops/img/rollout_staging_enabled.png Binary files differnew file mode 100644 index 00000000000..d0d1d356627 --- /dev/null +++ b/doc/topics/autodevops/img/rollout_staging_enabled.png diff --git a/doc/topics/autodevops/img/staging_enabled.png b/doc/topics/autodevops/img/staging_enabled.png Binary files differnew file mode 100644 index 00000000000..0ef1a67d641 --- /dev/null +++ b/doc/topics/autodevops/img/staging_enabled.png diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 5254e6e3d9a..33e2d710410 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -496,6 +496,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. | | `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` | | `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). | +| `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. | TIP: **Tip:** Set up the replica variables using a @@ -578,6 +579,57 @@ If `STAGING_ENABLED` is defined in your project (e.g., set `STAGING_ENABLED` to to a `staging` environment, and a `production_manual` job will be created for you when you're ready to manually deploy to production. +#### Incremental rollout to production **[PREMIUM]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5415) in GitLab 10.8. + +When you have a new version of your app to deploy in production, you may want +to use an incremental rollout to replace just a few pods with the latest code. +This will allow you to first check how the app is behaving, and later manually +increasing the rollout up to 100%. + +If `INCREMENTAL_ROLLOUT_ENABLED` is defined in your project (e.g., set +`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a secret variable), then instead of the +standard `production` job, 4 different +[manual jobs](../../ci/pipelines.md#manual-actions-from-the-pipeline-graph) +will be created: + +1. `rollout 10%` +1. `rollout 25%` +1. `rollout 50%` +1. `rollout 100%` + +The percentage is based on the `REPLICAS` variable and defines the number of +pods you want to have for your deployment. If you say `10`, and then you run +the `10%` rollout job, there will be `1` new pod + `9` old ones. + +To start a job, click on the play icon next to the job's name. You are not +required to go from `10%` to `100%`, you can jump to whatever job you want. +You can also scale down by running a lower percentage job, just before hitting +`100%`. Once you get to `100%`, you cannot scale down, and you'd have to roll +back by redeploying the old version using the +[rollback button](../../ci/environments.md#rolling-back-changes) in the +environment page. + +Below, you can see how the pipeline will look if the rollout or staging +variables are defined. + +- **Without `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`** + + ![Staging and rollout disabled](img/rollout_staging_disabled.png) + +- **Without `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`** + + ![Staging enabled](img/staging_enabled.png) + +- **With `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`** + + ![Rollout enabled](img/rollout_enabled.png) + +- **With `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`** + + ![Rollout and staging enabled](img/rollout_staging_enabled.png) + ## Currently supported languages NOTE: **Note:** diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index 15567715c98..0b16af2953b 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -23,6 +23,10 @@ page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the **Fork** button. Soon you should have a project under your namespace with the necessary files. +You can also start a new project from a +[GitLab project template](https://gitlab.com/gitlab-org/project-templates) if +you want to use a different language. + ## Setup your own cluster on Google Kubernetes Engine If you do not already have a Google Cloud account, create one at diff --git a/doc/user/admin_area/settings/img/enforce_terms.png b/doc/user/admin_area/settings/img/enforce_terms.png Binary files differindex e5f0a2683b5..e5f0a2683b5 100755..100644 --- a/doc/user/admin_area/settings/img/enforce_terms.png +++ b/doc/user/admin_area/settings/img/enforce_terms.png diff --git a/doc/user/admin_area/settings/img/respond_to_terms.png b/doc/user/admin_area/settings/img/respond_to_terms.png Binary files differindex d0d086c3498..d0d086c3498 100755..100644 --- a/doc/user/admin_area/settings/img/respond_to_terms.png +++ b/doc/user/admin_area/settings/img/respond_to_terms.png diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md index 2e1bd6bfe5c..930f2e73683 100644 --- a/doc/workflow/shortcuts.md +++ b/doc/workflow/shortcuts.md @@ -46,15 +46,19 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' | Keyboard Shortcut | Description | | ----------------- | ----------- | | <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page | -| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed | +| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project's activity feed | | <kbd>g</kbd> + <kbd>f</kbd> | Go to files | | <kbd>g</kbd> + <kbd>c</kbd> | Go to commits | -| <kbd>g</kbd> + <kbd>b</kbd> | Go to jobs | +| <kbd>g</kbd> + <kbd>j</kbd> | Go to jobs | | <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph | -| <kbd>g</kbd> + <kbd>g</kbd> | Go to repository charts | +| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts | | <kbd>g</kbd> + <kbd>i</kbd> | Go to issues | +| <kbd>g</kbd> + <kbd>b</kbd> | Go to issue boards | | <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests | +| <kbd>g</kbd> + <kbd>e</kbd> | Go to environments | +| <kbd>g</kbd> + <kbd>k</kbd> | Go to kubernetes | | <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets | +| <kbd>g</kbd> + <kbd>w</kbd> | Go to wiki | | <kbd>t</kbd> | Go to finding file | | <kbd>i</kbd> | New issue | diff --git a/lib/api/api.rb b/lib/api/api.rb index 5139e869c71..de20b2b8e67 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -8,14 +8,15 @@ module API PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze - use GrapeLogging::Middleware::RequestLogger, - logger: Logger.new(LOG_FILENAME), - formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, - include: [ - GrapeLogging::Loggers::FilterParameters.new, - GrapeLogging::Loggers::ClientEnv.new, - Gitlab::GrapeLogging::Loggers::UserLogger.new - ] + insert_before Grape::Middleware::Error, + GrapeLogging::Middleware::RequestLogger, + logger: Logger.new(LOG_FILENAME), + formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, + include: [ + GrapeLogging::Loggers::FilterParameters.new, + GrapeLogging::Loggers::ClientEnv.new, + Gitlab::GrapeLogging::Loggers::UserLogger.new + ] allow_access_with_scope :api prefix :api @@ -139,6 +140,7 @@ module API mount ::API::Keys mount ::API::Labels mount ::API::Lint + mount ::API::Markdown mount ::API::Members mount ::API::MergeRequestDiffs mount ::API::MergeRequests diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 92e3d5cc10a..03b6b30a0d8 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -165,9 +165,12 @@ module API group = find_group!(params[:id]) authorize! :admin_group, group + Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285') destroy_conditionally!(group) do |group| - ::Groups::DestroyService.new(group, current_user).execute + ::Groups::DestroyService.new(group, current_user).async_execute end + + accepted! end desc 'Get a list of projects in this group.' do diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb new file mode 100644 index 00000000000..b9ed68aa584 --- /dev/null +++ b/lib/api/markdown.rb @@ -0,0 +1,33 @@ +module API + class Markdown < Grape::API + params do + requires :text, type: String, desc: "The markdown text to render" + optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown" + optional :project, type: String, desc: "The full path of a project to use as the context when creating references using GitLab Flavored Markdown" + end + resource :markdown do + desc "Render markdown text" do + detail "This feature was introduced in GitLab 11.0." + end + post do + # Explicitly set CommonMark as markdown engine to use. + # Remove this set when https://gitlab.com/gitlab-org/gitlab-ce/issues/43011 is done. + context = { markdown_engine: :common_mark, only_path: false } + + if params[:project] + project = Project.find_by_full_path(params[:project]) + + not_found!("Project") unless can?(current_user, :read_project, project) + + context[:project] = project + else + context[:skip_project_check] = true + end + + context[:pipeline] = params[:gfm] ? :full : :plain_markdown + + { html: Banzai.render(params[:text], context) } + end + end + end +end diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 5f2a9567605..5cb96d467c0 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -14,7 +14,7 @@ module API use :pagination end get do - runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: %w(specific shared)) + runners = filter_runners(current_user.ci_owned_runners, params[:scope], without: %w(specific shared)) present paginate(runners), with: Entities::Runner end @@ -184,40 +184,35 @@ module API def authenticate_show_runner!(runner) return if runner.is_shared || current_user.admin? - forbidden!("No access granted") unless user_can_access_runner?(runner) + forbidden!("No access granted") unless can?(current_user, :read_runner, runner) end def authenticate_update_runner!(runner) return if current_user.admin? - forbidden!("Runner is shared") if runner.is_shared? - forbidden!("No access granted") unless user_can_access_runner?(runner) + forbidden!("No access granted") unless can?(current_user, :update_runner, runner) end def authenticate_delete_runner!(runner) return if current_user.admin? - forbidden!("Runner is shared") if runner.is_shared? forbidden!("Runner associated with more than one project") if runner.projects.count > 1 - forbidden!("No access granted") unless user_can_access_runner?(runner) + forbidden!("No access granted") unless can?(current_user, :delete_runner, runner) end def authenticate_enable_runner!(runner) - forbidden!("Runner is shared") if runner.is_shared? - forbidden!("Runner is locked") if runner.locked? + forbidden!("Runner is a group runner") if runner.group_type? + return if current_user.admin? - forbidden!("No access granted") unless user_can_access_runner?(runner) + forbidden!("Runner is locked") if runner.locked? + forbidden!("No access granted") unless can?(current_user, :assign_runner, runner) end def authenticate_list_runners_jobs!(runner) return if current_user.admin? - forbidden!("No access granted") unless user_can_access_runner?(runner) - end - - def user_can_access_runner?(runner) - current_user.ci_authorized_runners.exists?(runner.id) + forbidden!("No access granted") unless can?(current_user, :read_runner, runner) end end end diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb index 2c52d21fa1c..4fa7d196e50 100644 --- a/lib/api/v3/groups.rb +++ b/lib/api/v3/groups.rb @@ -131,7 +131,9 @@ module API delete ":id" do group = find_group!(params[:id]) authorize! :admin_group, group - present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user + ::Groups::DestroyService.new(group, current_user).async_execute + + accepted! end desc 'Get a list of projects in this group.' do diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb index c6d9957d452..8a5c46805bd 100644 --- a/lib/api/v3/runners.rb +++ b/lib/api/v3/runners.rb @@ -58,7 +58,7 @@ module API end def user_can_access_runner?(runner) - current_user.ci_authorized_runners.exists?(runner.id) + current_user.ci_owned_runners.exists?(runner.id) end end end diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb index f2e9a5a1116..4bc82ecb4d6 100644 --- a/lib/banzai/filter/gollum_tags_filter.rb +++ b/lib/banzai/filter/gollum_tags_filter.rb @@ -58,6 +58,9 @@ module Banzai def call doc.search(".//text()").each do |node| + # Do not perform linking inside <code> blocks + next unless node.ancestors('code').empty? + # A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running # before this one, it will be converted into `[[<em>TOC</em>]]`, so it # needs special-case handling diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index b9d5ecf70ec..2f023f4f242 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -73,7 +73,7 @@ module Banzai # # Note that while the key might exist, its value could be nil! def validate - needs :project + needs :project unless skip_project_check? end # Iterates over all <a> and text() nodes in a document. diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 8b2f05fffec..a1f24e8b093 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -42,9 +42,9 @@ module Banzai end def self.transform_context(context) - context.merge( - only_path: true, + context[:only_path] = true unless context.key?(:only_path) + context.merge( # EmojiFilter asset_host: Gitlab::Application.config.asset_host, asset_root: Gitlab.config.gitlab.base_url diff --git a/lib/gitlab/auth/ldap/access.rb b/lib/gitlab/auth/ldap/access.rb index 34286900e72..865185eb5db 100644 --- a/lib/gitlab/auth/ldap/access.rb +++ b/lib/gitlab/auth/ldap/access.rb @@ -6,7 +6,7 @@ module Gitlab module Auth module LDAP class Access - attr_reader :provider, :user + attr_reader :provider, :user, :ldap_identity def self.open(user, &block) Gitlab::Auth::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter| @@ -14,9 +14,12 @@ module Gitlab end end - def self.allowed?(user) + def self.allowed?(user, options = {}) self.open(user) do |access| + # Whether user is allowed, or not, we should update + # permissions to keep things clean if access.allowed? + access.update_user Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute true @@ -29,7 +32,8 @@ module Gitlab def initialize(user, adapter = nil) @adapter = adapter @user = user - @provider = user.ldap_identity.provider + @ldap_identity = user.ldap_identity + @provider = adapter&.provider || ldap_identity&.provider end def allowed? @@ -40,7 +44,7 @@ module Gitlab end # Block user in GitLab if he/she was blocked in AD - if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) + if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(ldap_identity.extern_uid, adapter) block_user(user, 'is disabled in Active Directory') false else @@ -64,27 +68,44 @@ module Gitlab Gitlab::Auth::LDAP::Config.new(provider) end + def find_ldap_user + Gitlab::Auth::LDAP::Person.find_by_dn(ldap_identity.extern_uid, adapter) + end + def ldap_user - @ldap_user ||= Gitlab::Auth::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter) + return unless provider + + @ldap_user ||= find_ldap_user end def block_user(user, reason) user.ldap_block - Gitlab::AppLogger.info( - "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \ - "blocking Gitlab user \"#{user.name}\" (#{user.email})" - ) + if provider + Gitlab::AppLogger.info( + "LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \ + "blocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + else + Gitlab::AppLogger.info( + "Account is not provided by LDAP, " \ + "blocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + end end def unblock_user(user, reason) user.activate Gitlab::AppLogger.info( - "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \ + "LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \ "unblocking Gitlab user \"#{user.name}\" (#{user.email})" ) end + + def update_user + # no-op in CE + end end end end diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb index 77185f52ced..d4415eaa6dc 100644 --- a/lib/gitlab/auth/ldap/config.rb +++ b/lib/gitlab/auth/ldap/config.rb @@ -11,6 +11,8 @@ module Gitlab attr_accessor :provider, :options + InvalidProvider = Class.new(StandardError) + def self.enabled? Gitlab.config.ldap.enabled end @@ -22,6 +24,10 @@ module Gitlab def self.available_servers return [] unless enabled? + _available_servers + end + + def self._available_servers Array.wrap(servers.first) end @@ -34,7 +40,7 @@ module Gitlab end def self.invalid_provider(provider) - raise "Unknown provider (#{provider}). Available providers: #{providers}" + raise InvalidProvider.new("Unknown provider (#{provider}). Available providers: #{providers}") end def initialize(provider) @@ -84,13 +90,17 @@ module Gitlab end def base - options['base'] + @base ||= Person.normalize_dn(options['base']) end def uid options['uid'] end + def label + options['label'] + end + def sync_ssh_keys? sync_ssh_keys.present? end @@ -132,6 +142,10 @@ module Gitlab options['timeout'].to_i end + def external_groups + options['external_groups'] || [] + end + def has_auth? options['password'] || options['bind_dn'] end diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb index 2760b1a3247..5fa9581f837 100644 --- a/lib/gitlab/auth/saml/config.rb +++ b/lib/gitlab/auth/saml/config.rb @@ -14,6 +14,10 @@ module Gitlab def external_groups options[:external_groups] end + + def admin_groups + options[:admin_groups] + end end end end diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb index cb01cd8004c..b8c84c37cd5 100644 --- a/lib/gitlab/auth/saml/user.rb +++ b/lib/gitlab/auth/saml/user.rb @@ -20,10 +20,8 @@ module Gitlab user ||= find_or_build_ldap_user if auto_link_ldap_user? user ||= build_new_user if signup_enabled? - if external_users_enabled? && user - # Check if there is overlap between the user's groups and the external groups - # setting then set user as external or internal. - user.external = !(auth_hash.groups & saml_config.external_groups).empty? + if user + user.external = !(auth_hash.groups & saml_config.external_groups).empty? if external_users_enabled? end user diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb index cf02030c577..4dc23f977da 100644 --- a/lib/gitlab/auth/user_auth_finders.rb +++ b/lib/gitlab/auth/user_auth_finders.rb @@ -1,9 +1,5 @@ module Gitlab module Auth - # - # Exceptions - # - AuthenticationError = Class.new(StandardError) MissingTokenError = Class.new(AuthenticationError) TokenNotFoundError = Class.new(AuthenticationError) @@ -61,6 +57,12 @@ module Gitlab private + def route_authentication_setting + return {} unless respond_to?(:route_setting) + + route_setting(:authentication) || {} + end + def access_token strong_memoize(:access_token) do find_oauth_access_token || find_personal_access_token diff --git a/lib/gitlab/ci/pipeline/expression.rb b/lib/gitlab/ci/pipeline/expression.rb new file mode 100644 index 00000000000..f57df7c5637 --- /dev/null +++ b/lib/gitlab/ci/pipeline/expression.rb @@ -0,0 +1,10 @@ +module Gitlab + module Ci + module Pipeline + module Expression + ExpressionError = Class.new(StandardError) + RuntimeError = Class.new(ExpressionError) + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb new file mode 100644 index 00000000000..10957598f76 --- /dev/null +++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb @@ -0,0 +1,29 @@ +module Gitlab + module Ci + module Pipeline + module Expression + module Lexeme + class Matches < Lexeme::Operator + PATTERN = /=~/.freeze + + def initialize(left, right) + @left = left + @right = right + end + + def evaluate(variables = {}) + text = @left.evaluate(variables) + regexp = @right.evaluate(variables) + + regexp.scan(text.to_s).any? + end + + def self.build(_value, behind, ahead) + new(behind, ahead) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb new file mode 100644 index 00000000000..9b239c29ea4 --- /dev/null +++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb @@ -0,0 +1,33 @@ +module Gitlab + module Ci + module Pipeline + module Expression + module Lexeme + require_dependency 're2' + + class Pattern < Lexeme::Value + PATTERN = %r{^/.+/[ismU]*$}.freeze + + def initialize(regexp) + @value = regexp + + unless Gitlab::UntrustedRegexp.valid?(@value) + raise Lexer::SyntaxError, 'Invalid regular expression!' + end + end + + def evaluate(variables = {}) + Gitlab::UntrustedRegexp.fabricate(@value) + rescue RegexpError + raise Expression::RuntimeError, 'Invalid regular expression!' + end + + def self.build(string) + new(string) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb index e1c68b7c3c2..4cacb1e62c9 100644 --- a/lib/gitlab/ci/pipeline/expression/lexer.rb +++ b/lib/gitlab/ci/pipeline/expression/lexer.rb @@ -5,15 +5,17 @@ module Gitlab class Lexer include ::Gitlab::Utils::StrongMemoize + SyntaxError = Class.new(Expression::ExpressionError) + LEXEMES = [ Expression::Lexeme::Variable, Expression::Lexeme::String, + Expression::Lexeme::Pattern, Expression::Lexeme::Null, - Expression::Lexeme::Equals + Expression::Lexeme::Equals, + Expression::Lexeme::Matches ].freeze - SyntaxError = Class.new(Statement::StatementError) - MAX_TOKENS = 100 def initialize(statement, max_tokens: MAX_TOKENS) diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb index 09a7c98464b..b36f1e0f865 100644 --- a/lib/gitlab/ci/pipeline/expression/statement.rb +++ b/lib/gitlab/ci/pipeline/expression/statement.rb @@ -3,15 +3,16 @@ module Gitlab module Pipeline module Expression class Statement - StatementError = Class.new(StandardError) + StatementError = Class.new(Expression::ExpressionError) GRAMMAR = [ + %w[variable], %w[variable equals string], %w[variable equals variable], %w[variable equals null], %w[string equals variable], %w[null equals variable], - %w[variable] + %w[variable matches pattern] ].freeze def initialize(statement, variables = {}) @@ -35,11 +36,13 @@ module Gitlab def truthful? evaluate.present? + rescue Expression::ExpressionError + false end def valid? parse_tree.is_a?(Lexeme::Base) - rescue StatementError + rescue Expression::ExpressionError false end end diff --git a/lib/gitlab/ci/pipeline/preloader.rb b/lib/gitlab/ci/pipeline/preloader.rb new file mode 100644 index 00000000000..e7a2e5511cf --- /dev/null +++ b/lib/gitlab/ci/pipeline/preloader.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + # Class for preloading data associated with pipelines such as commit + # authors. + module Preloader + def self.preload(pipelines) + # This ensures that all the pipeline commits are eager loaded before we + # start using them. + pipelines.each(&:commit) + + pipelines.each do |pipeline| + # This preloads the author of every commit. We're using "lazy_author" + # here since "author" immediately loads the data on the first call. + pipeline.commit.try(:lazy_author) + + # This preloads the number of warnings for every pipeline, ensuring + # that Ci::Pipeline#has_warnings? doesn't execute any additional + # queries. + pipeline.number_of_warnings + end + end + end + end + end +end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 76501dd50e8..d49d055c3f2 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -43,7 +43,7 @@ module Gitlab end def self.version - database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1] + @version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1] end def self.join_lateral_supported? diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb new file mode 100644 index 00000000000..3374203960e --- /dev/null +++ b/lib/gitlab/database/count.rb @@ -0,0 +1,48 @@ +# For large tables, PostgreSQL can take a long time to count rows due to MVCC. +# We can optimize this by using the reltuples count as described in https://wiki.postgresql.org/wiki/Slow_Counting. +module Gitlab + module Database + module Count + CONNECTION_ERRORS = + if defined?(PG) + [ + ActionView::Template::Error, + ActiveRecord::StatementInvalid, + PG::Error + ].freeze + else + [ + ActionView::Template::Error, + ActiveRecord::StatementInvalid + ].freeze + end + + def self.approximate_count(model) + return model.count unless Gitlab::Database.postgresql? + + execute_estimate_if_updated_recently(model) || model.count + end + + def self.execute_estimate_if_updated_recently(model) + ActiveRecord::Base.connection.select_value(postgresql_estimate_query(model)).to_i if reltuples_updated_recently?(model) + rescue *CONNECTION_ERRORS + end + + def self.reltuples_updated_recently?(model) + time = "to_timestamp(#{1.hour.ago.to_i})" + query = <<~SQL + SELECT 1 FROM pg_stat_user_tables WHERE relname = '#{model.table_name}' AND + (last_vacuum > #{time} OR last_autovacuum > #{time} OR last_analyze > #{time} OR last_autoanalyze > #{time}) + SQL + + ActiveRecord::Base.connection.select_all(query).count > 0 + rescue *CONNECTION_ERRORS + false + end + + def self.postgresql_estimate_query(model) + "SELECT reltuples::bigint AS estimate FROM pg_class where relname = '#{model.table_name}'" + end + end + end +end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index d78d29b7ac6..156d077a69c 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -104,25 +104,22 @@ module Gitlab # file.rb # oid: 4a # # - # Blob.find_entry_by_path(repo, '1a', 'app/file.rb') # => '4a' + # Blob.find_entry_by_path(repo, '1a', 'blog', 'app', 'file.rb') # => '4a' # - def find_entry_by_path(repository, root_id, path) + def find_entry_by_path(repository, root_id, *path_parts) root_tree = repository.lookup(root_id) - # Strip leading slashes - path[%r{^/*}] = '' - path_arr = path.split('/') entry = root_tree.find do |entry| - entry[:name] == path_arr[0] + entry[:name] == path_parts[0] end return nil unless entry - if path_arr.size > 1 + if path_parts.size > 1 return nil unless entry[:type] == :tree - path_arr.shift - find_entry_by_path(repository, entry[:oid], path_arr.join('/')) + path_parts.shift + find_entry_by_path(repository, entry[:oid], *path_parts) else [:blob, :commit].include?(entry[:type]) ? entry : nil end @@ -185,10 +182,13 @@ module Gitlab def find_by_rugged(repository, sha, path, limit:) return unless path + # Strip any leading / characters from the path + path = path.sub(%r{\A/*}, '') + rugged_commit = repository.lookup(sha) root_tree = rugged_commit.tree - blob_entry = find_entry_by_path(repository, root_tree.oid, path) + blob_entry = find_entry_by_path(repository, root_tree.oid, *path.split('/')) return nil unless blob_entry diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index fabcd46c8e9..89f761dd515 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -6,6 +6,7 @@ module Gitlab attr_accessor :raw_commit, :head + MAX_COMMIT_MESSAGE_DISPLAY_SIZE = 10.megabytes MIN_SHA_LENGTH = 7 SERIALIZE_KEYS = [ :id, :message, :parent_ids, @@ -63,9 +64,7 @@ module Gitlab if is_enabled repo.gitaly_commit_client.find_commit(commit_id) else - obj = repo.rev_parse_target(commit_id) - - obj.is_a?(Rugged::Commit) ? obj : nil + rugged_find(repo, commit_id) end end @@ -76,6 +75,12 @@ module Gitlab nil end + def rugged_find(repo, commit_id) + obj = repo.rev_parse_target(commit_id) + + obj.is_a?(Rugged::Commit) ? obj : nil + end + # Get last commit for HEAD # # Ex. @@ -297,11 +302,40 @@ module Gitlab nil end end + + def get_message(repository, commit_id) + BatchLoader.for({ repository: repository, commit_id: commit_id }).batch do |items, loader| + items_by_repo = items.group_by { |i| i[:repository] } + + items_by_repo.each do |repo, items| + commit_ids = items.map { |i| i[:commit_id] } + + messages = get_messages(repository, commit_ids) + + messages.each do |commit_sha, message| + loader.call({ repository: repository, commit_id: commit_sha }, message) + end + end + end + end + + def get_messages(repository, commit_ids) + repository.gitaly_migrate(:commit_messages) do |is_enabled| + if is_enabled + repository.gitaly_commit_client.get_commit_messages(commit_ids) + else + commit_ids.map { |id| [id, rugged_find(repository, id).message] }.to_h + end + end + end end def initialize(repository, raw_commit, head = nil) raise "Nil as raw commit passed" unless raw_commit + @repository = repository + @head = head + case raw_commit when Hash init_from_hash(raw_commit) @@ -312,9 +346,6 @@ module Gitlab else raise "Invalid raw commit type: #{raw_commit.class}" end - - @repository = repository - @head = head end def sha @@ -342,21 +373,6 @@ module Gitlab parent_ids.first end - # Shows the diff between the commit's parent and the commit. - # - # Cuts out the header and stats from #to_patch and returns only the diff. - # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324 - def to_diff - Gitlab::GitalyClient.migrate(:commit_patch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - @repository.gitaly_commit_client.patch(id) - else - rugged_diff_from_parent.patch - end - end - end - # Returns a diff object for the changes from this commit's first parent. # If there is no parent, then the diff is between this commit and an # empty repo. See Repository#diff for keys allowed in the +options+ @@ -432,16 +448,6 @@ module Gitlab Gitlab::Git::CommitStats.new(@repository, self) end - def to_patch(options = {}) - begin - rugged_commit.to_mbox(options) - rescue Rugged::InvalidError => ex - if ex.message =~ /commit \w+ is a merge commit/i - 'Patch format is not currently supported for merge commits.' - end - end - end - # Get ref names collection # # Ex. @@ -543,7 +549,7 @@ module Gitlab # TODO: Once gitaly "takes over" Rugged consider separating the # subject from the message to make it clearer when there's one # available but not the other. - @message = (commit.body.presence || commit.subject).dup + @message = message_from_gitaly_body @authored_date = Time.at(commit.author.date.seconds).utc @author_name = commit.author.name.dup @author_email = commit.author.email.dup @@ -595,6 +601,25 @@ module Gitlab def refs(repo) repo.refs_hash[id] end + + def message_from_gitaly_body + return @raw_commit.subject.dup if @raw_commit.body_size.zero? + return @raw_commit.body.dup if full_body_fetched_from_gitaly? + + if @raw_commit.body_size > MAX_COMMIT_MESSAGE_DISPLAY_SIZE + "#{@raw_commit.subject}\n\n--commit message is too big".strip + else + fetch_body_from_gitaly + end + end + + def full_body_fetched_from_gitaly? + @raw_commit.body.bytesize == @raw_commit.body_size + end + + def fetch_body_from_gitaly + self.class.get_message(@repository, id) + end end end end diff --git a/lib/gitlab/git/path_helper.rb b/lib/gitlab/git/path_helper.rb index 155cf52f050..57b82a37d6c 100644 --- a/lib/gitlab/git/path_helper.rb +++ b/lib/gitlab/git/path_helper.rb @@ -6,7 +6,7 @@ module Gitlab class << self def normalize_path(filename) # Strip all leading slashes so that //foo -> foo - filename[%r{^/*}] = '' + filename = filename.sub(%r{\A/*}, '') # Expand relative paths (e.g. foo/../bar) filename = Pathname.new(filename) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index c7a755319dc..2be604a5b13 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1473,10 +1473,19 @@ module Gitlab def search_files_by_content(query, ref) return [] if empty? || query.blank? - offset = 2 - args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) + safe_query = Regexp.escape(query) + ref ||= root_ref + + gitaly_migrate(:search_files_by_content) do |is_enabled| + if is_enabled + gitaly_repository_client.search_files_by_content(ref, safe_query) + else + offset = 2 + args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{safe_query} #{ref}) - run_git(args).first.scrub.split(/^--\n/) + run_git(args).first.scrub.split(/^--\n/) + end + end end def can_be_merged?(source_sha, target_branch) @@ -1491,12 +1500,19 @@ module Gitlab def search_files_by_name(query, ref) safe_query = Regexp.escape(query.sub(%r{^/*}, "")) + ref ||= root_ref return [] if empty? || safe_query.blank? - args = %W(ls-tree -r --name-status --full-tree #{ref || root_ref} -- #{safe_query}) + gitaly_migrate(:search_files_by_name) do |is_enabled| + if is_enabled + gitaly_repository_client.search_files_by_name(ref, safe_query) + else + args = %W(ls-tree -r --name-status --full-tree #{ref} -- #{safe_query}) - run_git(args).first.lines.map(&:strip) + run_git(args).first.lines.map(&:strip) + end + end end def find_commits_by_message(query, ref, path, limit, offset) @@ -1572,14 +1588,12 @@ module Gitlab end def checksum - gitaly_migrate(:calculate_checksum, - status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - gitaly_repository_client.calculate_checksum - else - calculate_checksum_by_shelling_out - end - end + # The exists? RPC is much cheaper, so we perform this request first + raise NoRepository, "Repository does not exists" unless exists? + + gitaly_repository_client.calculate_checksum + rescue GRPC::NotFound + raise NoRepository # Guard against data races. end private @@ -1948,7 +1962,12 @@ module Gitlab end target_commit = Gitlab::Git::Commit.find(self, ref.target) - Gitlab::Git::Tag.new(self, ref.name, ref.target, target_commit, message) + Gitlab::Git::Tag.new(self, { + name: ref.name, + target: ref.target, + target_commit: target_commit, + message: message + }) end.sort_by(&:name) end @@ -2348,7 +2367,7 @@ module Gitlab end def gitaly_delete_refs(*ref_names) - gitaly_ref_client.delete_refs(refs: ref_names) + gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any? end def rugged_remove_remote(remote_name) @@ -2478,36 +2497,6 @@ module Gitlab rev_parse_target(ref).oid end - def calculate_checksum_by_shelling_out - raise NoRepository unless exists? - - args = %W(--git-dir=#{path} show-ref --heads --tags) - output, status = run_git(args) - - if status.nil? || !status.zero? - # Non-valid git repositories return 128 as the status code and an error output - raise InvalidRepository if status == 128 && output.to_s.downcase =~ /not a git repository/ - # Empty repositories returns with a non-zero status and an empty output. - raise ChecksumError, output unless output.blank? - - return EMPTY_REPOSITORY_CHECKSUM - end - - refs = output.split("\n") - - result = refs.inject(nil) do |checksum, ref| - value = Digest::SHA1.hexdigest(ref).hex - - if checksum.nil? - value - else - checksum ^ value - end - end - - result.to_s(16) - end - def build_git_cmd(*args) object_directories = alternate_object_directories.join(File::PATH_SEPARATOR) diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb index 8a01f92e2af..e35ea5762eb 100644 --- a/lib/gitlab/git/repository_mirroring.rb +++ b/lib/gitlab/git/repository_mirroring.rb @@ -35,7 +35,11 @@ module Gitlab next if name =~ /\^\{\}\Z/ target_commit = Gitlab::Git::Commit.find(self, target) - Gitlab::Git::Tag.new(self, name, target, target_commit) + Gitlab::Git::Tag.new(self, { + name: name, + target: target, + target_commit: target_commit + }) end.compact end diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb index 8a8f7b051ed..e44284572fd 100644 --- a/lib/gitlab/git/tag.rb +++ b/lib/gitlab/git/tag.rb @@ -1,17 +1,99 @@ module Gitlab module Git class Tag < Ref - attr_reader :object_sha + extend Gitlab::EncodingHelper + + attr_reader :object_sha, :repository + + MAX_TAG_MESSAGE_DISPLAY_SIZE = 10.megabytes + SERIALIZE_KEYS = %i[name target target_commit message].freeze + + attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator + + class << self + def get_message(repository, tag_id) + BatchLoader.for({ repository: repository, tag_id: tag_id }).batch do |items, loader| + items_by_repo = items.group_by { |i| i[:repository] } + + items_by_repo.each do |repo, items| + tag_ids = items.map { |i| i[:tag_id] } + + messages = get_messages(repository, tag_ids) + + messages.each do |id, message| + loader.call({ repository: repository, tag_id: id }, message) + end + end + end + end + + def get_messages(repository, tag_ids) + repository.gitaly_migrate(:tag_messages) do |is_enabled| + if is_enabled + repository.gitaly_ref_client.get_tag_messages(tag_ids) + else + tag_ids.map do |id| + tag = repository.rugged.lookup(id) + message = tag.is_a?(Rugged::Commit) ? "" : tag.message + + [id, message] + end.to_h + end + end + end + end + + def initialize(repository, raw_tag) + @repository = repository + @raw_tag = raw_tag + + case raw_tag + when Hash + init_from_hash + when Gitaly::Tag + init_from_gitaly + end - def initialize(repository, name, target, target_commit, message = nil) super(repository, name, target, target_commit) + end + + def init_from_hash + raw_tag = @raw_tag.symbolize_keys + + SERIALIZE_KEYS.each do |key| + send("#{key}=", raw_tag[key]) # rubocop:disable GitlabSecurity/PublicSend + end + end + + def init_from_gitaly + @name = encode!(@raw_tag.name.dup) + @target = @raw_tag.id + @message = message_from_gitaly_tag - @message = message + if @raw_tag.target_commit.present? + @target_commit = Gitlab::Git::Commit.decorate(repository, @raw_tag.target_commit) + end end def message encode! @message end + + private + + def message_from_gitaly_tag + return @raw_tag.message.dup if full_message_fetched_from_gitaly? + + if @raw_tag.message_size > MAX_TAG_MESSAGE_DISPLAY_SIZE + '--tag message is too big' + else + self.class.get_message(@repository, target) + end + end + + def full_message_fetched_from_gitaly? + @raw_tag.message.bytesize == @raw_tag.message_size + end end end end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index a36e6c822f9..1f5f88bf792 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -334,6 +334,22 @@ module Gitlab signatures end + def get_commit_messages(commit_ids) + request = Gitaly::GetCommitMessagesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids) + response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_messages, request) + + messages = Hash.new { |h, k| h[k] = ''.b } + current_commit_id = nil + + response.each do |rpc_message| + current_commit_id = rpc_message.commit_id if rpc_message.commit_id.present? + + messages[current_commit_id] << rpc_message.message + end + + messages + end + private def call_commit_diff(request_params, options = {}) diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 831cfd1e014..44b0e517bf0 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -40,7 +40,7 @@ module Gitlab raise Gitlab::Git::Repository::TagExistsError end - Util.gitlab_tag_from_gitaly_tag(@repository, response.tag) + Gitlab::Git::Tag.new(@repository, response.tag) rescue GRPC::FailedPrecondition => e raise Gitlab::Git::Repository::InvalidRef, e end diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index ba6b577fd17..3ac46be6208 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -171,6 +171,22 @@ module Gitlab consume_ref_contains_sha_response(stream, :branch_names) end + def get_tag_messages(tag_ids) + request = Gitaly::GetTagMessagesRequest.new(repository: @gitaly_repo, tag_ids: tag_ids) + response = GitalyClient.call(@repository.storage, :ref_service, :get_tag_messages, request) + + messages = Hash.new { |h, k| h[k] = ''.b } + current_tag_id = nil + + response.each do |rpc_message| + current_tag_id = rpc_message.tag_id if rpc_message.tag_id.present? + + messages[current_tag_id] << rpc_message.message + end + + messages + end + private def consume_refs_response(response) @@ -210,7 +226,7 @@ module Gitlab def consume_tags_response(response) response.flat_map do |message| - message.tags.map { |gitaly_tag| Util.gitlab_tag_from_gitaly_tag(@repository, gitaly_tag) } + message.tags.map { |gitaly_tag| Gitlab::Git::Tag.new(@repository, gitaly_tag) } end end diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 132a5947f17..ee01f5a5bd9 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -301,6 +301,16 @@ module Gitlab GitalyClient.call(@storage, :repository_service, :get_raw_changes, request) end + + def search_files_by_name(ref, query) + request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: query) + GitalyClient.call(@storage, :repository_service, :search_files_by_name, request).flat_map(&:files) + end + + def search_files_by_content(ref, query) + request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query) + GitalyClient.call(@storage, :repository_service, :search_files_by_content, request).flat_map(&:matches) + end end end end diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb index 405567db94a..9c19c51d412 100644 --- a/lib/gitlab/gitaly_client/util.rb +++ b/lib/gitlab/gitaly_client/util.rb @@ -21,20 +21,6 @@ module Gitlab gitaly_repository.relative_path, gitaly_repository.gl_repository) end - - def gitlab_tag_from_gitaly_tag(repository, gitaly_tag) - if gitaly_tag.target_commit.present? - commit = Gitlab::Git::Commit.decorate(repository, gitaly_tag.target_commit) - end - - Gitlab::Git::Tag.new( - repository, - Gitlab::EncodingHelper.encode!(gitaly_tag.name.dup), - gitaly_tag.id, - commit, - Gitlab::EncodingHelper.encode!(gitaly_tag.message.chomp) - ) - end end end end diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index c9122a23568..d323cb9dadf 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -57,7 +57,7 @@ module Gitlab regex = Regexp.escape(wildcard_address) regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)') - Regexp.new(regex).freeze + Regexp.new(/\A#{regex}\z/).freeze end end end diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb index 75ba0799058..dc2d91dfa23 100644 --- a/lib/gitlab/untrusted_regexp.rb +++ b/lib/gitlab/untrusted_regexp.rb @@ -9,7 +9,9 @@ module Gitlab # there is a strict limit on total execution time. See the RE2 documentation # at https://github.com/google/re2/wiki/Syntax for more details. class UntrustedRegexp - delegate :===, to: :regexp + require_dependency 're2' + + delegate :===, :source, to: :regexp def initialize(pattern, multiline: false) if multiline @@ -35,6 +37,10 @@ module Gitlab RE2.Replace(text, regexp, rewrite) end + def ==(other) + self.source == other.source + end + # Handles regular expressions with the preferred RE2 library where possible # via UntustedRegex. Falls back to Ruby's built-in regular expression library # when the syntax would be invalid in RE2. @@ -48,6 +54,24 @@ module Gitlab Regexp.new(pattern) end + def self.valid?(pattern) + !!self.fabricate(pattern) + rescue RegexpError + false + end + + def self.fabricate(pattern) + matches = pattern.match(%r{^/(?<regexp>.+)/(?<flags>[ismU]*)$}) + + raise RegexpError, 'Invalid regular expression!' if matches.nil? + + expression = matches[:regexp] + flags = matches[:flags] + expression.prepend("(?#{flags})") if flags.present? + + self.new(expression, multiline: false) + end + private attr_reader :regexp diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake index af30ecb0e9b..e7aab50e42a 100644 --- a/lib/tasks/migrate/setup_postgresql.rake +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -8,6 +8,7 @@ task setup_postgresql: :environment do require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like') require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb') require Rails.root.join('db/migrate/20180215181245_users_name_lower_index.rb') + require Rails.root.join('db/migrate/20180504195842_project_name_lower_index.rb') require Rails.root.join('db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb') NamespacesProjectsPathLowerIndexes.new.up @@ -18,5 +19,6 @@ task setup_postgresql: :environment do IndexRedirectRoutesPathForLike.new.up AddIndexOnNamespacesLowerName.new.up UsersNameLowerIndex.new.up + ProjectNameLowerIndex.new.up AddPathIndexToRedirectRoutes.new.up end diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb index fbf9a4d17e5..0931e649e24 100644 --- a/qa/qa/specs/features/merge_request/create_spec.rb +++ b/qa/qa/specs/features/merge_request/create_spec.rb @@ -11,7 +11,7 @@ module QA expect(page).to have_content('This is a merge request') expect(page).to have_content('Great feature') - expect(page).to have_content('Opened less than a minute ago') + expect(page).to have_content(/Opened [\w\s]+ a minute ago/) end end end diff --git a/scripts/create_mysql_user.sh b/scripts/create_mysql_user.sh index 286b1325f1d..35f68c581f3 100644 --- a/scripts/create_mysql_user.sh +++ b/scripts/create_mysql_user.sh @@ -1,7 +1,6 @@ #!/bin/bash mysql --user=root --host=mysql <<EOF -CREATE DATABASE IF NOT EXISTS gitlabhq_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER IF NOT EXISTS 'gitlab'@'%'; GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%'; FLUSH PRIVILEGES; diff --git a/scripts/create_postgres_user.sh b/scripts/create_postgres_user.sh index 8a744df3226..8a049bcd7fb 100644 --- a/scripts/create_postgres_user.sh +++ b/scripts/create_postgres_user.sh @@ -1,8 +1,6 @@ #!/bin/bash psql -h postgres -U postgres postgres <<EOF -DROP DATABASE IF EXISTS gitlabhq_test; -CREATE DATABASE gitlabhq_test; CREATE USER gitlab; -GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gitlab; EOF diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 206d62dbc78..d8bcc9f8191 100644 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -49,20 +49,8 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml cp config/redis.shared_state.yml.example config/redis.shared_state.yml sed -i 's/localhost/redis/g' config/redis.shared_state.yml -# Some tasks (e.g. db:seed_fu) need to have a properly-configured database -# user but not necessarily a full schema loaded -if [ "$CREATE_DB_USER" != "false" ]; then - if [ "$GITLAB_DATABASE" = 'postgresql' ]; then - . scripts/create_postgres_user.sh - else - . scripts/create_mysql_user.sh - fi -fi - if [ "$SETUP_DB" != "false" ]; then - bundle exec rake db:drop db:create db:schema:load db:migrate - - if [ "$GITLAB_DATABASE" = "mysql" ]; then - bundle exec rake add_limits_mysql - fi + setup_db +elif getent hosts postgres || getent hosts mysql; then + setup_db_user_only fi diff --git a/scripts/utils.sh b/scripts/utils.sh index 6faa701f0ce..2d2ba115563 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -12,3 +12,21 @@ retry() { done return 1 } + +setup_db_user_only() { + if [ "$GITLAB_DATABASE" = "postgresql" ]; then + . scripts/create_postgres_user.sh + else + . scripts/create_mysql_user.sh + fi +} + +setup_db() { + setup_db_user_only + + bundle exec rake db:drop db:create db:schema:load db:migrate + + if [ "$GITLAB_DATABASE" = "mysql" ]; then + bundle exec rake add_limits_mysql + fi +} diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 694c64ae1ad..003fec8ac68 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -79,41 +79,18 @@ describe Projects::CommitController do end describe "as diff" do - include_examples "export as", :diff - let(:format) { :diff } + it "triggers workhorse to serve the request" do + go(id: commit.id, format: :diff) - it "should really only be a git diff" do - go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format) - - expect(response.body).to start_with("diff --git") - end - - it "is only be a git diff without whitespace changes" do - go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1) - - expect(response.body).to start_with("diff --git") - - # without whitespace option, there are more than 2 diff_splits for other formats - diff_splits = assigns(:diffs).diff_files.first.diff.diff.split("\n") - expect(diff_splits.length).to be <= 2 + expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:") end end describe "as patch" do - include_examples "export as", :patch - let(:format) { :patch } - let(:commit2) { project.commit('498214de67004b1da3d820901307bed2a68a8ef6') } - - it "is a git email patch" do - go(id: commit2.id, format: format) - - expect(response.body).to start_with("From #{commit2.id}") - end - it "contains a git diff" do - go(id: commit2.id, format: format) + go(id: commit.id, format: :patch) - expect(response.body).to match(/^diff --git/) + expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:") end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index a451bbb97b6..9e7bc20a6d1 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -35,10 +35,16 @@ describe Projects::PipelinesController do expect(json_response).to include('pipelines') expect(json_response['pipelines'].count).to eq 4 - expect(json_response['count']['all']).to eq 4 - expect(json_response['count']['running']).to eq 1 - expect(json_response['count']['pending']).to eq 1 - expect(json_response['count']['finished']).to eq 1 + expect(json_response['count']['all']).to eq '4' + expect(json_response['count']['running']).to eq '1' + expect(json_response['count']['pending']).to eq '1' + expect(json_response['count']['finished']).to eq '1' + end + + it 'does not include coverage data for the pipelines' do + subject + + expect(json_response['pipelines'][0]).not_to include('coverage') end context 'when performing gitaly calls', :request_store do diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb index a91c868cbaf..f1810763d2d 100644 --- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb @@ -19,11 +19,11 @@ describe Projects::Settings::CiCdController do end context 'with group runners' do - let(:group_runner) { create(:ci_runner) } + let(:group_runner) { create(:ci_runner, runner_type: :group_type) } let(:parent_group) { create(:group) } let(:group) { create(:group, runners: [group_runner], parent: parent_group) } let(:other_project) { create(:project, group: group) } - let!(:project_runner) { create(:ci_runner, projects: [other_project]) } + let!(:project_runner) { create(:ci_runner, projects: [other_project], runner_type: :project_type) } let!(:shared_runner) { create(:ci_runner, :shared) } it 'sets assignable project runners only' do @@ -31,7 +31,7 @@ describe Projects::Settings::CiCdController do get :show, namespace_id: project.namespace, project_id: project - expect(assigns(:assignable_runners)).to eq [project_runner] + expect(assigns(:assignable_runners)).to contain_exactly(project_runner) end end end diff --git a/spec/factories/gitaly/tag.rb b/spec/factories/gitaly/tag.rb new file mode 100644 index 00000000000..e99776d524a --- /dev/null +++ b/spec/factories/gitaly/tag.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :gitaly_tag, class: Gitaly::Tag do + skip_create + + name { 'v3.1.4' } + message { 'Pie release' } + target_commit factory: :gitaly_commit + end +end diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index e4be6193b8b..a986ddc4abc 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -5,18 +5,41 @@ describe 'Invites' do let(:owner) { create(:user, name: 'John Doe') } let(:group) { create(:group, name: 'Owned') } let(:project) { create(:project, :repository, namespace: group) } - let(:invite) { group.group_members.invite.last } + let(:group_invite) { group.group_members.invite.last } before do project.add_master(owner) group.add_user(owner, Gitlab::Access::OWNER) group.add_developer('user@example.com', owner) - invite.generate_invite_token! + group_invite.generate_invite_token! + end + + def confirm_email_and_sign_in(new_user) + new_user_token = User.find_by_email(new_user.email).confirmation_token + + visit user_confirmation_path(confirmation_token: new_user_token) + fill_in_sign_in_form(new_user) + end + + def fill_in_sign_up_form(new_user) + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email + fill_in 'new_user_password', with: new_user.password + click_button "Register" + end + + def fill_in_sign_in_form(user) + fill_in 'user_login', with: user.email + fill_in 'user_password', with: user.password + check 'user_remember_me' + click_button 'Sign in' end context 'when signed out' do before do - visit invite_path(invite.raw_invite_token) + visit invite_path(group_invite.raw_invite_token) end it 'renders sign in page with sign in notice' do @@ -25,12 +48,9 @@ describe 'Invites' do end it 'sign in and redirects to invitation page' do - fill_in 'user_login', with: user.email - fill_in 'user_password', with: user.password - check 'user_remember_me' - click_button 'Sign in' + fill_in_sign_in_form(user) - expect(current_path).to eq(invite_path(invite.raw_invite_token)) + expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) expect(page).to have_content( 'You have been invited by John Doe to join group Owned as Developer.' ) @@ -45,7 +65,7 @@ describe 'Invites' do end it 'shows message user already a member' do - visit invite_path(invite.raw_invite_token) + visit invite_path(group_invite.raw_invite_token) expect(page).to have_content('However, you are already a member of this group.') end end @@ -53,7 +73,7 @@ describe 'Invites' do describe 'accepting the invitation' do before do sign_in(user) - visit invite_path(invite.raw_invite_token) + visit invite_path(group_invite.raw_invite_token) end it 'grants access and redirects to group page' do @@ -69,7 +89,7 @@ describe 'Invites' do context 'when signed in' do before do sign_in(user) - visit invite_path(invite.raw_invite_token) + visit invite_path(group_invite.raw_invite_token) end it 'declines application and redirects to dashboard' do @@ -83,7 +103,7 @@ describe 'Invites' do context 'when signed out' do before do - visit decline_invite_path(invite.raw_invite_token) + visit decline_invite_path(group_invite.raw_invite_token) end it 'declines application and redirects to sign in page' do @@ -94,4 +114,72 @@ describe 'Invites' do end end end + + describe 'invite an user using their email address' do + let(:new_user) { build_stubbed(:user) } + let(:invite_email) { new_user.email } + let(:group_invite) { create(:group_member, :invited, group: group, invite_email: invite_email) } + let!(:project_invite) { create(:project_member, :invited, project: project, invite_email: invite_email) } + + before do + stub_application_setting(send_user_confirmation_email: send_email_confirmation) + visit invite_path(group_invite.raw_invite_token) + end + + context 'email confirmation disabled' do + let(:send_email_confirmation) { false } + + it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do + fill_in_sign_up_form(new_user) + + expect(current_path).to eq(dashboard_projects_path) + expect(page).to have_content(project.full_name) + visit group_path(group) + expect(page).to have_content(group.full_name) + end + + context 'the user sign-up using a different email address' do + let(:invite_email) { build_stubbed(:user).email } + + it 'signs up and redirects to the invitation page' do + fill_in_sign_up_form(new_user) + + expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) + end + end + end + + context 'email confirmation enabled' do + let(:send_email_confirmation) { true } + + it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do + fill_in_sign_up_form(new_user) + confirm_email_and_sign_in(new_user) + + expect(current_path).to eq(root_path) + expect(page).to have_content(project.full_name) + visit group_path(group) + expect(page).to have_content(group.full_name) + end + + it "doesn't accept invitations until the user confirm his email" do + fill_in_sign_up_form(new_user) + sign_in(owner) + + visit project_project_members_path(project) + expect(page).to have_content 'Invited' + end + + context 'the user sign-up using a different email address' do + let(:invite_email) { build_stubbed(:user).email } + + it 'signs up and redirects to the invitation page' do + fill_in_sign_up_form(new_user) + confirm_email_and_sign_in(new_user) + + expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) + end + end + end + end end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 5248a783db4..f9defa22d35 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -42,6 +42,22 @@ feature 'Environments page', :js do expect(page).to have_content('You don\'t have any environments right now') end end + + context 'when cluster is not reachable' do + let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + let!(:application_prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } + + before do + allow_any_instance_of(Kubeclient::Client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) + end + + it 'should show one environment without error' do + visit_environments(project, scope: 'available') + + expect(page).to have_css('.environments-container') + expect(page.all('.environment-name').length).to eq(1) + end + end end describe 'with one stopped environment' do diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb index 47c5a8161d9..495a010b32c 100644 --- a/spec/features/projects/user_uses_shortcuts_spec.rb +++ b/spec/features/projects/user_uses_shortcuts_spec.rb @@ -13,6 +13,8 @@ describe 'User uses shortcuts', :js do context 'when navigating to the Project pages' do it 'redirects to the details page' do + visit project_issues_path(project) + find('body').native.send_key('g') find('body').native.send_key('p') @@ -22,7 +24,7 @@ describe 'User uses shortcuts', :js do it 'redirects to the activity page' do find('body').native.send_key('g') - find('body').native.send_key('e') + find('body').native.send_key('v') expect(page).to have_active_navigation('Project') expect(page).to have_active_sub_navigation('Activity') @@ -72,10 +74,19 @@ describe 'User uses shortcuts', :js do expect(page).to have_active_sub_navigation('List') end + it 'redirects to the issue board page' do + find('body').native.send_key('g') + find('body').native.send_key('b') + + expect(page).to have_active_navigation('Issues') + expect(page).to have_active_sub_navigation('Board') + end + it 'redirects to the new issue page' do find('body').native.send_key('i') expect(page).to have_content(project.title) + expect(page).to have_content('New Issue') end end @@ -88,6 +99,34 @@ describe 'User uses shortcuts', :js do end end + context 'when navigating to the CI / CD pages' do + it 'redirects to the Jobs page' do + find('body').native.send_key('g') + find('body').native.send_key('j') + + expect(page).to have_active_navigation('CI / CD') + expect(page).to have_active_sub_navigation('Jobs') + end + end + + context 'when navigating to the Operations pages' do + it 'redirects to the Environments page' do + find('body').native.send_key('g') + find('body').native.send_key('e') + + expect(page).to have_active_navigation('Operations') + expect(page).to have_active_sub_navigation('Environments') + end + + it 'redirects to the Kubernetes page' do + find('body').native.send_key('g') + find('body').native.send_key('k') + + expect(page).to have_active_navigation('Operations') + expect(page).to have_active_sub_navigation('Kubernetes') + end + end + context 'when navigating to the Snippets pages' do it 'redirects to the snippets page' do find('body').native.send_key('g') diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 6586ccaa400..e473739a6aa 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -155,4 +155,27 @@ feature 'Projects > Wiki > User previews markdown changes', :js do end end end + + it "does not linkify double brackets inside code blocks as expected" do + click_link 'New page' + page.within '#modal-new-wiki' do + fill_in :new_wiki_path, with: 'linkify_test' + click_button 'Create page' + end + + page.within '.wiki-form' do + fill_in :wiki_content, with: <<-HEREDOC + `[[do_not_linkify]]` + ``` + [[also_do_not_linkify]] + ``` + HEREDOC + click_on "Preview" + end + + expect(page).to have_content("do_not_linkify") + + expect(page.html).to include('[[do_not_linkify]]') + expect(page.html).to include('[[also_do_not_linkify]]') + end end diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index 622a1e40d07..05922df6b81 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -17,6 +17,7 @@ "required": [ "id", "color", + "text_color", "description", "title", "priority" @@ -29,6 +30,7 @@ }, "description": { "type": ["string", "null"] }, "title": { "type": "string" }, + "title": { "text_color": "string" }, "priority": { "type": ["integer", "null"] } } }, diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index d646bef96f5..568e679abe9 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -1,13 +1,19 @@ import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import actionComponent from '~/pipelines/components/graph/action_component.vue'; -import eventHub from '~/pipelines/event_hub'; import mountComponent from '../../helpers/vue_mount_component_helper'; describe('pipeline graph action component', () => { let component; + let mock; beforeEach(done => { const ActionComponent = Vue.extend(actionComponent); + mock = new MockAdapter(axios); + + mock.onPost('foo.json').reply(200); + component = mountComponent(ActionComponent, { tooltipText: 'bar', link: 'foo', @@ -18,15 +24,10 @@ describe('pipeline graph action component', () => { }); afterEach(() => { + mock.restore(); component.$destroy(); }); - it('should emit an event with the provided link', () => { - eventHub.$on('postAction', link => { - expect(link).toEqual('foo'); - }); - }); - it('should render the provided title as a bootstrap tooltip', () => { expect(component.$el.getAttribute('data-original-title')).toEqual('bar'); }); @@ -34,10 +35,12 @@ describe('pipeline graph action component', () => { it('should update bootstrap tooltip when title changes', done => { component.tooltipText = 'changed'; - setTimeout(() => { + component.$nextTick() + .then(() => { expect(component.$el.getAttribute('data-original-title')).toBe('changed'); - done(); - }); + }) + .then(done) + .catch(done.fail); }); it('should render an svg', () => { @@ -45,44 +48,18 @@ describe('pipeline graph action component', () => { expect(component.$el.querySelector('svg')).toBeDefined(); }); - it('disables the button when clicked', done => { - component.$el.click(); + describe('on click', () => { + it('emits `pipelineActionRequestComplete` after a successfull request', done => { + spyOn(component, '$emit'); - component.$nextTick(() => { - expect(component.$el.getAttribute('disabled')).toEqual('disabled'); - done(); - }); - }); - - it('re-enabled the button when `requestFinishedFor` matches `linkRequested`', done => { - component.$el.click(); - - component - .$nextTick() - .then(() => { - expect(component.$el.getAttribute('disabled')).toEqual('disabled'); - component.requestFinishedFor = 'foo'; - }) - .then(() => { - expect(component.$el.getAttribute('disabled')).toBeNull(); - }) - .then(done) - .catch(done.fail); - }); - - it('does not re-enable the button when `requestFinishedFor` does not matches `linkRequested`', done => { - component.$el.click(); + component.$el.click(); - component - .$nextTick() - .then(() => { - expect(component.$el.getAttribute('disabled')).toEqual('disabled'); - component.requestFinishedFor = 'bar'; - }) - .then(() => { - expect(component.$el.getAttribute('disabled')).toEqual('disabled'); - }) - .then(done) - .catch(done.fail); + component.$nextTick() + .then(() => { + expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete'); + }) + .then(done) + .catch(done.fail); + }); }); }); diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index 75156e7bdfd..16f6db39d6a 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -102,4 +102,31 @@ describe('Pipelines stage component', () => { }); }); }); + + describe('pipelineActionRequestComplete', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + + mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + }); + + describe('within pipeline table', () => { + it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', done => { + spyOn(eventHub, '$emit'); + + component.type = 'PIPELINES_TABLE'; + component.$el.querySelector('button').click(); + + setTimeout(() => { + component.$el.querySelector('.js-ci-action').click(); + component.$nextTick() + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + }) + .then(done) + .catch(done.fail); + }, 0); + }); + }); + }); }); diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb index ca76d6f0881..0e178b859c4 100644 --- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb +++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb @@ -91,6 +91,12 @@ describe Banzai::Filter::GollumTagsFilter do expect(doc.at_css('a').text).to eq 'link-text' expect(doc.at_css('a')['href']).to eq expected_path end + + it "inside back ticks will be exempt from linkification" do + doc = filter('<code>[[link-in-backticks]]</code>', project_wiki: project_wiki) + + expect(doc.at_css('code').text).to eq '[[link-in-backticks]]' + end end context 'table of contents' do diff --git a/spec/lib/gitlab/auth/ldap/access_spec.rb b/spec/lib/gitlab/auth/ldap/access_spec.rb index 6b251d824f7..eff21985108 100644 --- a/spec/lib/gitlab/auth/ldap/access_spec.rb +++ b/spec/lib/gitlab/auth/ldap/access_spec.rb @@ -8,6 +8,7 @@ describe Gitlab::Auth::LDAP::Access do describe '.allowed?' do it 'updates the users `last_credential_check_at' do + allow(access).to receive(:update_user) expect(access).to receive(:allowed?) { true } expect(described_class).to receive(:open).and_yield(access) @@ -16,12 +17,21 @@ describe Gitlab::Auth::LDAP::Access do end end + describe '#find_ldap_user' do + it 'finds a user by dn first' do + expect(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user) + + access.find_ldap_user + end + end + describe '#allowed?' do subject { access.allowed? } context 'when the user cannot be found' do before do allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).and_return(nil) end it { is_expected.to be_falsey } @@ -54,7 +64,7 @@ describe Gitlab::Auth::LDAP::Access do end end - context 'and has no disabled flag in active diretory' do + context 'and has no disabled flag in active directory' do before do allow(Gitlab::Auth::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false) end @@ -100,6 +110,7 @@ describe Gitlab::Auth::LDAP::Access do context 'when user cannot be found' do before do allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).and_return(nil) end it { is_expected.to be_falsey } diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb index 82587e2ba55..d3ab599d5a0 100644 --- a/spec/lib/gitlab/auth/ldap/config_spec.rb +++ b/spec/lib/gitlab/auth/ldap/config_spec.rb @@ -23,7 +23,7 @@ describe Gitlab::Auth::LDAP::Config do end it 'raises an error if a unknown provider is used' do - expect { described_class.new 'unknown' }.to raise_error(RuntimeError) + expect { described_class.new 'unknown' }.to raise_error(described_class::InvalidProvider) end end @@ -370,4 +370,38 @@ describe Gitlab::Auth::LDAP::Config do }) end end + + describe '#base' do + context 'when the configured base is not normalized' do + it 'returns the normalized base' do + stub_ldap_config(options: { 'base' => 'DC=example, DC= com' }) + + expect(config.base).to eq('dc=example,dc=com') + end + end + + context 'when the configured base is normalized' do + it 'returns the base unaltered' do + stub_ldap_config(options: { 'base' => 'dc=example,dc=com' }) + + expect(config.base).to eq('dc=example,dc=com') + end + end + + context 'when the configured base is malformed' do + it 'returns the base unaltered' do + stub_ldap_config(options: { 'base' => 'invalid,dc=example,dc=com' }) + + expect(config.base).to eq('invalid,dc=example,dc=com') + end + end + + context 'when the configured base is blank' do + it 'returns the base unaltered' do + stub_ldap_config(options: { 'base' => '' }) + + expect(config.base).to eq('') + end + end + end end diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb index 653c19942ea..44bb9d20e47 100644 --- a/spec/lib/gitlab/auth/ldap/user_spec.rb +++ b/spec/lib/gitlab/auth/ldap/user_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::Auth::LDAP::User do + include LdapHelpers + let(:ldap_user) { described_class.new(auth_hash) } let(:gl_user) { ldap_user.gl_user } let(:info) do @@ -177,8 +179,7 @@ describe Gitlab::Auth::LDAP::User do describe 'blocking' do def configure_block(value) - allow_any_instance_of(Gitlab::Auth::LDAP::Config) - .to receive(:block_auto_created_users).and_return(value) + stub_ldap_config(block_auto_created_users: value) end context 'signup' do diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 08718c382b9..83d39b82068 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -111,7 +111,15 @@ describe Gitlab::Ci::Config::Entry::Policy do context 'when specifying invalid variables expressions token' do let(:config) { { variables: ['$MY_VAR == 123'] } } - it 'reports an error about invalid statement' do + it 'reports an error about invalid expression' do + expect(entry.errors).to include /invalid expression syntax/ + end + end + + context 'when using invalid variables expressions regexp' do + let(:config) { { variables: ['$MY_VAR =~ /some ( thing/'] } } + + it 'reports an error about invalid expression' do expect(entry.errors).to include /invalid expression syntax/ end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb new file mode 100644 index 00000000000..49e5af52f4d --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb @@ -0,0 +1,80 @@ +require 'fast_spec_helper' +require_dependency 're2' + +describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do + let(:left) { double('left') } + let(:right) { double('right') } + + describe '.build' do + it 'creates a new instance of the token' do + expect(described_class.build('=~', left, right)) + .to be_a(described_class) + end + end + + describe '.type' do + it 'is an operator' do + expect(described_class.type).to eq :operator + end + end + + describe '#evaluate' do + it 'returns false when left and right do not match' do + allow(left).to receive(:evaluate).and_return('my-string') + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('something')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq false + end + + it 'returns true when left and right match' do + allow(left).to receive(:evaluate).and_return('my-awesome-string') + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('awesome.string$')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq true + end + + it 'supports matching against a nil value' do + allow(left).to receive(:evaluate).and_return(nil) + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('pattern')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq false + end + + it 'supports multiline strings' do + allow(left).to receive(:evaluate).and_return <<~TEXT + My awesome contents + + My-text-string! + TEXT + + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('text-string')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq true + end + + it 'supports regexp flags' do + allow(left).to receive(:evaluate).and_return <<~TEXT + My AWESOME content + TEXT + + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('(?i)awesome')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq true + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb new file mode 100644 index 00000000000..3ebc2e94727 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb @@ -0,0 +1,96 @@ +require 'fast_spec_helper' + +describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do + describe '.build' do + it 'creates a new instance of the token' do + expect(described_class.build('/.*/')) + .to be_a(described_class) + end + + it 'raises an error if pattern is invalid' do + expect { described_class.build('/ some ( thin/i') } + .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError) + end + end + + describe '.type' do + it 'is a value lexeme' do + expect(described_class.type).to eq :value + end + end + + describe '.scan' do + it 'correctly identifies a pattern token' do + scanner = StringScanner.new('/pattern/') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('pattern') + end + + it 'is a greedy scanner for regexp boundaries' do + scanner = StringScanner.new('/some .* / pattern/') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('some .* / pattern') + end + + it 'does not allow to use an empty pattern' do + scanner = StringScanner.new(%(//)) + + token = described_class.scan(scanner) + + expect(token).to be_nil + end + + it 'support single flag' do + scanner = StringScanner.new('/pattern/i') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('(?i)pattern') + end + + it 'support multiple flags' do + scanner = StringScanner.new('/pattern/im') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('(?im)pattern') + end + + it 'does not support arbitrary flags' do + scanner = StringScanner.new('/pattern/x') + + token = described_class.scan(scanner) + + expect(token).to be_nil + end + end + + describe '#evaluate' do + it 'returns a regular expression' do + regexp = described_class.new('/abc/') + + expect(regexp.evaluate).to eq Gitlab::UntrustedRegexp.new('abc') + end + + it 'raises error if evaluated regexp is not valid' do + allow(Gitlab::UntrustedRegexp).to receive(:valid?).and_return(true) + + regexp = described_class.new('/invalid ( .*/') + + expect { regexp.evaluate } + .to raise_error(Gitlab::Ci::Pipeline::Expression::RuntimeError) + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb index 230ceeb07f8..3f11b3f7673 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do end describe '#tokens' do - it 'tokenss single value' do + it 'returns single value' do tokens = described_class.new('$VARIABLE').tokens expect(tokens).to be_one @@ -20,14 +20,14 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do expect(tokens).to all(be_an_instance_of(token_class)) end - it 'tokenss multiple values of the same token' do + it 'returns multiple values of the same token' do tokens = described_class.new("$VARIABLE1 $VARIABLE2").tokens expect(tokens.size).to eq 2 expect(tokens).to all(be_an_instance_of(token_class)) end - it 'tokenss multiple values with different tokens' do + it 'returns multiple values with different tokens' do tokens = described_class.new('$VARIABLE "text" "value"').tokens expect(tokens.size).to eq 3 @@ -36,7 +36,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do expect(tokens.third.value).to eq '"value"' end - it 'tokenss tokens and operators' do + it 'returns tokens and operators' do tokens = described_class.new('$VARIABLE == "text"').tokens expect(tokens.size).to eq 3 diff --git a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb index e8e6f585310..2b78b1dd4a7 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require 'fast_spec_helper' describe Gitlab::Ci::Pipeline::Expression::Parser do describe '#tree' do diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb index 6685bf5385b..11e73294f18 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +require 'fast_spec_helper' +require 'rspec-parameterized' describe Gitlab::Ci::Pipeline::Expression::Statement do subject do @@ -36,7 +37,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do '== "123"', # invalid left side '"some string"', # only string provided '$VAR ==', # invalid right side - '12345', # unknown syntax + 'null', # missing lexemes '' # empty statement ] @@ -44,7 +45,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do context "when expression grammar is #{syntax.inspect}" do let(:text) { syntax } - it 'aises a statement error exception' do + it 'raises a statement error exception' do expect { subject.parse_tree } .to raise_error described_class::StatementError end @@ -82,48 +83,66 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do end describe '#evaluate' do - statements = [ - ['$PRESENT_VARIABLE == "my variable"', true], - ["$PRESENT_VARIABLE == 'my variable'", true], - ['"my variable" == $PRESENT_VARIABLE', true], - ['$PRESENT_VARIABLE == null', false], - ['$EMPTY_VARIABLE == null', false], - ['"" == $EMPTY_VARIABLE', true], - ['$EMPTY_VARIABLE', ''], - ['$UNDEFINED_VARIABLE == null', true], - ['null == $UNDEFINED_VARIABLE', true], - ['$PRESENT_VARIABLE', 'my variable'], - ['$UNDEFINED_VARIABLE', nil] - ] - - statements.each do |expression, value| - context "when using expression `#{expression}`" do - let(:text) { expression } - - it "evaluates to `#{value.inspect}`" do - expect(subject.evaluate).to eq value - end + using RSpec::Parameterized::TableSyntax + + where(:expression, :value) do + '$PRESENT_VARIABLE == "my variable"' | true + '"my variable" == $PRESENT_VARIABLE' | true + '$PRESENT_VARIABLE == null' | false + '$EMPTY_VARIABLE == null' | false + '"" == $EMPTY_VARIABLE' | true + '$EMPTY_VARIABLE' | '' + '$UNDEFINED_VARIABLE == null' | true + 'null == $UNDEFINED_VARIABLE' | true + '$PRESENT_VARIABLE' | 'my variable' + '$UNDEFINED_VARIABLE' | nil + "$PRESENT_VARIABLE =~ /var.*e$/" | true + "$PRESENT_VARIABLE =~ /^var.*/" | false + "$EMPTY_VARIABLE =~ /var.*/" | false + "$UNDEFINED_VARIABLE =~ /var.*/" | false + "$PRESENT_VARIABLE =~ /VAR.*/i" | true + end + + with_them do + let(:text) { expression } + + it "evaluates to `#{params[:value].inspect}`" do + expect(subject.evaluate).to eq value end end end describe '#truthful?' do - statements = [ - ['$PRESENT_VARIABLE == "my variable"', true], - ["$PRESENT_VARIABLE == 'no match'", false], - ['$UNDEFINED_VARIABLE == null', true], - ['$PRESENT_VARIABLE', true], - ['$UNDEFINED_VARIABLE', false], - ['$EMPTY_VARIABLE', false] - ] - - statements.each do |expression, value| - context "when using expression `#{expression}`" do - let(:text) { expression } - - it "returns `#{value.inspect}`" do - expect(subject.truthful?).to eq value - end + using RSpec::Parameterized::TableSyntax + + where(:expression, :value) do + '$PRESENT_VARIABLE == "my variable"' | true + "$PRESENT_VARIABLE == 'no match'" | false + '$UNDEFINED_VARIABLE == null' | true + '$PRESENT_VARIABLE' | true + '$UNDEFINED_VARIABLE' | false + '$EMPTY_VARIABLE' | false + '$INVALID = 1' | false + "$PRESENT_VARIABLE =~ /var.*/" | true + "$UNDEFINED_VARIABLE =~ /var.*/" | false + end + + with_them do + let(:text) { expression } + + it "returns `#{params[:value].inspect}`" do + expect(subject.truthful?).to eq value + end + end + + context 'when evaluating expression raises an error' do + let(:text) { '$PRESENT_VARIABLE' } + + it 'returns false' do + allow(subject).to receive(:evaluate) + .and_raise(described_class::StatementError) + + expect(subject.truthful?).to be_falsey end end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb index 6d7453f0de5..cedfe270f9d 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require 'fast_spec_helper' describe Gitlab::Ci::Pipeline::Expression::Token do let(:value) { '$VARIABLE' } diff --git a/spec/lib/gitlab/ci/pipeline/preloader_spec.rb b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb new file mode 100644 index 00000000000..477c7477df0 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Preloader do + describe '.preload' do + it 'preloads the author of every pipeline commit' do + commit = double(:commit) + pipeline = double(:pipeline, commit: commit) + + expect(commit) + .to receive(:lazy_author) + + expect(pipeline) + .to receive(:number_of_warnings) + + described_class.preload([pipeline]) + end + end +end diff --git a/spec/lib/gitlab/database/count_spec.rb b/spec/lib/gitlab/database/count_spec.rb new file mode 100644 index 00000000000..9d9caaabe16 --- /dev/null +++ b/spec/lib/gitlab/database/count_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Gitlab::Database::Count do + before do + create_list(:project, 3) + end + + describe '.execute_estimate_if_updated_recently', :postgresql do + context 'when reltuples have not been updated' do + before do + expect(described_class).to receive(:reltuples_updated_recently?).and_return(false) + end + + it 'returns nil' do + expect(described_class.execute_estimate_if_updated_recently(Project)).to be nil + end + end + + context 'when reltuples have been updated' do + before do + ActiveRecord::Base.connection.execute('ANALYZE projects') + end + + it 'calls postgresql_estimate_query' do + expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original + expect(described_class.execute_estimate_if_updated_recently(Project)).to eq(3) + end + end + end + + describe '.approximate_count' do + context 'when reltuples have not been updated' do + it 'counts all projects the normal way' do + allow(described_class).to receive(:reltuples_updated_recently?).and_return(false) + + expect(Project).to receive(:count).and_call_original + expect(described_class.approximate_count(Project)).to eq(3) + end + end + + context 'no permission' do + it 'falls back to standard query' do + allow(described_class).to receive(:reltuples_updated_recently?).and_raise(PG::InsufficientPrivilege) + + expect(Project).to receive(:count).and_call_original + expect(described_class.approximate_count(Project)).to eq(3) + end + end + + describe 'when reltuples have been updated', :postgresql do + before do + ActiveRecord::Base.connection.execute('ANALYZE projects') + end + + it 'counts all projects in the fast way' do + expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original + + expect(described_class.approximate_count(Project)).to eq(3) + end + end + end +end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 1fe1d3926ad..8ac36ae8bab 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -32,6 +32,12 @@ describe Gitlab::Database do end describe '.version' do + around do |example| + described_class.instance_variable_set(:@version, nil) + example.run + described_class.instance_variable_set(:@version, nil) + end + context "on mysql" do it "extracts the version number" do allow(described_class).to receive(:database_version) @@ -49,6 +55,14 @@ describe Gitlab::Database do expect(described_class.version).to eq '9.4.4' end end + + it 'memoizes the result' do + count = ActiveRecord::QueryRecorder + .new { 2.times { described_class.version } } + .count + + expect(count).to eq(1) + end end describe '.join_lateral_supported?' do diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index a05feaac1ca..08c6d1e55e9 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -66,7 +66,8 @@ describe Gitlab::Git::Commit, seed_helper: true do describe "Commit info from gitaly commit" do let(:subject) { "My commit".force_encoding('ASCII-8BIT') } let(:body) { subject + "My body".force_encoding('ASCII-8BIT') } - let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body) } + let(:body_size) { body.length } + let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body, body_size: body_size) } let(:id) { gitaly_commit.id } let(:committer) { gitaly_commit.committer } let(:author) { gitaly_commit.author } @@ -83,10 +84,30 @@ describe Gitlab::Git::Commit, seed_helper: true do it { expect(commit.committer_email).to eq(committer.email) } it { expect(commit.parent_ids).to eq(gitaly_commit.parent_ids) } - context 'no body' do + context 'body_size != body.size' do let(:body) { "".force_encoding('ASCII-8BIT') } - it { expect(commit.safe_message).to eq(subject) } + context 'zero body_size' do + it { expect(commit.safe_message).to eq(subject) } + end + + context 'body_size less than threshold' do + let(:body_size) { 123 } + + it 'fetches commit message seperately' do + expect(described_class).to receive(:get_message).with(repository, id) + + commit.safe_message + end + end + + context 'body_size greater than threshold' do + let(:body_size) { described_class::MAX_COMMIT_MESSAGE_DISPLAY_SIZE + 1 } + + it 'returns the suject plus a notice about message size' do + expect(commit.safe_message).to eq("My commit\n\n--commit message is too big") + end + end end end @@ -554,24 +575,10 @@ describe Gitlab::Git::Commit, seed_helper: true do it_should_behave_like '#stats' end - describe '#to_diff' do - subject { commit.to_diff } - - it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" } - it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} - end - describe '#has_zero_stats?' do it { expect(commit.has_zero_stats?).to eq(false) } end - describe '#to_patch' do - subject { commit.to_patch } - - it { is_expected.to include "From #{SeedRepo::Commit::ID}" } - it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} - end - describe '#to_hash' do let(:hash) { commit.to_hash } subject { hash } @@ -603,6 +610,35 @@ describe Gitlab::Git::Commit, seed_helper: true do it { is_expected.not_to include("feature") } end + describe '.get_message' do + let(:commit_ids) { %w[6d394385cf567f80a8fd85055db1ab4c5295806f cfe32cf61b73a0d5e9f13e774abde7ff789b1660] } + + subject do + commit_ids.map { |id| described_class.get_message(repository, id) } + end + + shared_examples 'getting commit messages' do + it 'gets commit messages' do + expect(subject).to contain_exactly( + "Added contributing guide\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", + "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n" + ) + end + end + + context 'when Gitaly commit_messages feature is enabled' do + it_behaves_like 'getting commit messages' + + it 'gets messages in one batch', :request_store do + expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1) + end + end + + context 'when Gitaly commit_messages feature is disabled', :disable_gitaly do + it_behaves_like 'getting commit messages' + end + end + def sample_commit_hash { author_email: "dmitriy.zaporozhets@gmail.com", diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 740d12c443d..af6a486ab20 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -596,6 +596,10 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + it 'does not fail when deleting an empty list of refs' do + expect { repo.delete_refs(*[]) }.not_to raise_error + end + it 'raises an error if it failed' do expect { repo.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::Repository::GitError) end @@ -2243,66 +2247,42 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#checksum' do - shared_examples 'calculating checksum' do - it 'calculates the checksum for non-empty repo' do - expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4' - end - - it 'returns 0000000000000000000000000000000000000000 for an empty repo' do - FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git')) - - system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git), - chdir: storage_path, - out: '/dev/null', - err: '/dev/null') + it 'calculates the checksum for non-empty repo' do + expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4' + end - empty_repo = described_class.new('default', 'empty-repo.git', '') + it 'returns 0000000000000000000000000000000000000000 for an empty repo' do + FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git')) - expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000' - end + system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git), + chdir: storage_path, + out: '/dev/null', + err: '/dev/null') - it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do - FileUtils.rm_rf(File.join(storage_path, 'non-valid.git')) + empty_repo = described_class.new('default', 'empty-repo.git', '') - system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git), - chdir: SEED_STORAGE_PATH, - out: '/dev/null', - err: '/dev/null') + expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000' + end - File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0) + it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do + FileUtils.rm_rf(File.join(storage_path, 'non-valid.git')) - non_valid = described_class.new('default', 'non-valid.git', '') + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git), + chdir: SEED_STORAGE_PATH, + out: '/dev/null', + err: '/dev/null') - expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository) - end + File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0) - it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do - broken_repo = described_class.new('default', 'a/path.git', '') + non_valid = described_class.new('default', 'non-valid.git', '') - expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository) - end + expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository) end - context 'when calculate_checksum Gitaly feature is enabled' do - it_behaves_like 'calculating checksum' - end - - context 'when calculate_checksum Gitaly feature is disabled', :disable_gitaly do - it_behaves_like 'calculating checksum' - - describe 'when storage is broken', :broken_storage do - it 'raises a storage exception when storage is not available' do - broken_repo = described_class.new('broken', 'a/path.git', '') - - expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible) - end - end - - it "raises a Gitlab::Git::Repository::Failure error if the `popen` call to git returns a non-zero exit code" do - allow(repository).to receive(:popen).and_return(['output', nil]) + it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do + broken_repo = described_class.new('default', 'a/path.git', '') - expect { repository.checksum }.to raise_error Gitlab::Git::Repository::ChecksumError - end + expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository) end end diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb index 6c4f538bf01..be2f5bfb819 100644 --- a/spec/lib/gitlab/git/tag_spec.rb +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -32,4 +32,56 @@ describe Gitlab::Git::Tag, seed_helper: true do context 'when Gitaly tags feature is disabled', :skip_gitaly_mock do it_behaves_like 'Gitlab::Git::Repository#tags' end + + describe '.get_message' do + let(:tag_ids) { %w[f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b] } + + subject do + tag_ids.map { |id| described_class.get_message(repository, id) } + end + + shared_examples 'getting tag messages' do + it 'gets tag messages' do + expect(subject[0]).to eq("Release\n") + expect(subject[1]).to eq("Version 1.1.0\n") + end + end + + context 'when Gitaly tag_messages feature is enabled' do + it_behaves_like 'getting tag messages' + + it 'gets messages in one batch', :request_store do + expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1) + end + end + + context 'when Gitaly tag_messages feature is disabled', :disable_gitaly do + it_behaves_like 'getting tag messages' + end + end + + describe 'tag into from Gitaly tag' do + context 'message_size != message.size' do + let(:gitaly_tag) { build(:gitaly_tag, message: ''.b, message_size: message_size) } + let(:tag) { described_class.new(repository, gitaly_tag) } + + context 'message_size less than threshold' do + let(:message_size) { 123 } + + it 'fetches tag message seperately' do + expect(described_class).to receive(:get_message).with(repository, gitaly_tag.id) + + tag.message + end + end + + context 'message_size greater than threshold' do + let(:message_size) { described_class::MAX_TAG_MESSAGE_DISPLAY_SIZE + 1 } + + it 'returns a notice about message size' do + expect(tag.message).to eq("--tag message is too big") + end + end + end + end end diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index ad087f42e06..4c0c3fcbcc7 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -83,6 +83,10 @@ describe Gitlab::IncomingEmail do it "returns reply key" do expect(described_class.key_from_address("replies+key@example.com")).to eq("key") end + + it 'does not match emails with extra bits' do + expect(described_class.key_from_address('somereplies+somekey@example.com.someotherdomain.com')).to be nil + end end context 'self.key_from_fallback_message_id' do diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb index 0ee7fa1e570..0a6ac0aa294 100644 --- a/spec/lib/gitlab/untrusted_regexp_spec.rb +++ b/spec/lib/gitlab/untrusted_regexp_spec.rb @@ -1,6 +1,49 @@ -require 'spec_helper' +require 'fast_spec_helper' +require 'support/shared_examples/malicious_regexp_shared_examples' describe Gitlab::UntrustedRegexp do + describe '.valid?' do + it 'returns true if regexp is valid' do + expect(described_class.valid?('/some ( thing/')) + .to be false + end + + it 'returns true if regexp is invalid' do + expect(described_class.valid?('/some .* thing/')) + .to be true + end + end + + describe '.fabricate' do + context 'when regexp is using /regexp/ scheme with flags' do + it 'fabricates regexp with a single flag' do + regexp = described_class.fabricate('/something/i') + + expect(regexp).to eq described_class.new('(?i)something') + expect(regexp.scan('SOMETHING')).to be_one + end + + it 'fabricates regexp with multiple flags' do + regexp = described_class.fabricate('/something/im') + + expect(regexp).to eq described_class.new('(?im)something') + end + + it 'fabricates regexp without flags' do + regexp = described_class.fabricate('/something/') + + expect(regexp).to eq described_class.new('something') + end + end + + context 'when regexp is a raw pattern' do + it 'raises an error' do + expect { described_class.fabricate('some .* thing') } + .to raise_error(RegexpError) + end + end + end + describe '#initialize' do subject { described_class.new(pattern) } diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 84ddbbbf2ee..8a52c151cc4 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -594,7 +594,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{project.full_name} project" is_expected.to have_html_escaped_body_text project.full_name - is_expected.to have_body_text project.web_url + is_expected.to have_body_text project.full_name is_expected.to have_body_text project_member.human_access is_expected.to have_body_text project_member.invite_token end diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb index 56b5d616284..5489c17bd82 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -5,7 +5,7 @@ describe Appearance do it { is_expected.to be_valid } - it { is_expected.to have_many(:uploads).dependent(:destroy) } + it { is_expected.to have_many(:uploads) } describe '.current', :use_clean_rails_memory_store_caching do let!(:appearance) { create(:appearance) } @@ -41,4 +41,12 @@ describe Appearance do expect(new_row.valid?).to eq(false) end end + + context 'with uploads' do + it_behaves_like 'model with mounted uploader', false do + let(:model_object) { create(:appearance, :with_logo) } + let(:upload_attribute) { :logo } + let(:uploader_class) { AttachmentUploader } + end + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index dc810489011..7d8bddbcedb 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1270,6 +1270,46 @@ describe Ci::Build do end end + describe '#playable?' do + context 'when build is a manual action' do + context 'when build has been skipped' do + subject { build_stubbed(:ci_build, :manual, status: :skipped) } + + it { is_expected.not_to be_playable } + end + + context 'when build has been canceled' do + subject { build_stubbed(:ci_build, :manual, status: :canceled) } + + it { is_expected.to be_playable } + end + + context 'when build is successful' do + subject { build_stubbed(:ci_build, :manual, status: :success) } + + it { is_expected.to be_playable } + end + + context 'when build has failed' do + subject { build_stubbed(:ci_build, :manual, status: :failed) } + + it { is_expected.to be_playable } + end + + context 'when build is a manual untriggered action' do + subject { build_stubbed(:ci_build, :manual, status: :manual) } + + it { is_expected.to be_playable } + end + end + + context 'when build is not a manual action' do + subject { build_stubbed(:ci_build, :success) } + + it { is_expected.not_to be_playable } + end + end + describe 'project settings' do describe '#allow_git_fetch' do it 'return project allow_git_fetch configuration' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index ddd66a6be87..e7845b693a1 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -774,6 +774,33 @@ describe Ci::Pipeline, :mailer do end end + describe '#number_of_warnings' do + it 'returns the number of warnings' do + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop') + + expect(pipeline.number_of_warnings).to eq(1) + end + + it 'supports eager loading of the number of warnings' do + pipeline2 = create(:ci_empty_pipeline, status: :created, project: project) + + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop') + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline2, name: 'rubocop') + + pipelines = project.pipelines.to_a + + pipelines.each(&:number_of_warnings) + + # To run the queries we need to actually use the lazy objects, which we do + # by just sending "to_i" to them. + amount = ActiveRecord::QueryRecorder + .new { pipelines.each { |p| p.number_of_warnings.to_i } } + .count + + expect(amount).to eq(1) + end + end + shared_context 'with some outdated pipelines' do before do create_pipeline(:canceled, 'ref', 'A', project) diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index e2b212f4f4c..0fbc934f669 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -626,62 +626,26 @@ describe Ci::Runner do end describe '.assignable_for' do - let(:runner) { create(:ci_runner) } + let!(:unlocked_project_runner) { create(:ci_runner, runner_type: :project_type, projects: [project]) } + let!(:locked_project_runner) { create(:ci_runner, runner_type: :project_type, locked: true, projects: [project]) } + let!(:group_runner) { create(:ci_runner, runner_type: :group_type) } + let!(:instance_runner) { create(:ci_runner, :shared) } let(:project) { create(:project) } let(:another_project) { create(:project) } - before do - project.runners << runner - end - - context 'with shared runners' do - before do - runner.update(is_shared: true) - end - - context 'does not give owned runner' do - subject { described_class.assignable_for(project) } - - it { is_expected.to be_empty } - end - - context 'does not give shared runner' do - subject { described_class.assignable_for(another_project) } - - it { is_expected.to be_empty } - end - end - - context 'with unlocked runner' do - context 'does not give owned runner' do - subject { described_class.assignable_for(project) } - - it { is_expected.to be_empty } - end + context 'with already assigned project' do + subject { described_class.assignable_for(project) } - context 'does give a specific runner' do - subject { described_class.assignable_for(another_project) } - - it { is_expected.to contain_exactly(runner) } - end + it { is_expected.to be_empty } end - context 'with locked runner' do - before do - runner.update(locked: true) - end - - context 'does not give owned runner' do - subject { described_class.assignable_for(project) } - - it { is_expected.to be_empty } - end - - context 'does not give a locked runner' do - subject { described_class.assignable_for(another_project) } + context 'with a different project' do + subject { described_class.assignable_for(another_project) } - it { is_expected.to be_empty } - end + it { is_expected.to include(unlocked_project_runner) } + it { is_expected.not_to include(group_runner) } + it { is_expected.not_to include(locked_project_runner) } + it { is_expected.not_to include(instance_runner) } end end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index aeca6ee903a..407e2fc598a 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -85,6 +85,16 @@ describe Clusters::Applications::Prometheus do it 'copies options and headers from kube client to proxy client' do expect(subject.prometheus_client.options).to eq(kube_client.rest_client.options.merge(headers: kube_client.headers)) end + + context 'when cluster is not reachable' do + before do + allow(kube_client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) + end + + it 'returns nil' do + expect(subject.prometheus_client).to be_nil + end + end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 4e6b037a720..090f91168ad 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -52,22 +52,98 @@ describe Commit do end end - describe '#author' do + describe '#author', :request_store do it 'looks up the author in a case-insensitive way' do user = create(:user, email: commit.author_email.upcase) expect(commit.author).to eq(user) end - it 'caches the author', :request_store do + it 'caches the author' do user = create(:user, email: commit.author_email) - expect(User).to receive(:find_by_any_email).and_call_original expect(commit.author).to eq(user) + key = "Commit:author:#{commit.author_email.downcase}" - expect(RequestStore.store[key]).to eq(user) + expect(RequestStore.store[key]).to eq(user) expect(commit.author).to eq(user) end + + context 'using eager loading' do + let!(:alice) { create(:user, email: 'alice@example.com') } + let!(:bob) { create(:user, email: 'hunter2@example.com') } + + let(:alice_commit) do + described_class.new(RepoHelpers.sample_commit, project).tap do |c| + c.author_email = 'alice@example.com' + end + end + + let(:bob_commit) do + # The commit for Bob uses one of his alternative Emails, instead of the + # primary one. + described_class.new(RepoHelpers.sample_commit, project).tap do |c| + c.author_email = 'bob@example.com' + end + end + + let(:eve_commit) do + described_class.new(RepoHelpers.sample_commit, project).tap do |c| + c.author_email = 'eve@example.com' + end + end + + let!(:commits) { [alice_commit, bob_commit, eve_commit] } + + before do + create(:email, user: bob, email: 'bob@example.com') + end + + it 'executes only two SQL queries' do + recorder = ActiveRecord::QueryRecorder.new do + # Running this first ensures we don't run one query for every + # commit. + commits.each(&:lazy_author) + + # This forces the execution of the SQL queries necessary to load the + # data. + commits.each { |c| c.author.try(:id) } + end + + expect(recorder.count).to eq(2) + end + + it "preloads the authors for Commits matching a user's primary Email" do + commits.each(&:lazy_author) + + expect(alice_commit.author).to eq(alice) + end + + it "preloads the authors for Commits using a User's alternative Email" do + commits.each(&:lazy_author) + + expect(bob_commit.author).to eq(bob) + end + + it 'sets the author to Nil if an author could not be found for a Commit' do + commits.each(&:lazy_author) + + expect(eve_commit.author).to be_nil + end + + it 'does not execute SQL queries once the authors are preloaded' do + commits.each(&:lazy_author) + commits.each { |c| c.author.try(:id) } + + recorder = ActiveRecord::QueryRecorder.new do + alice_commit.author + bob_commit.author + eve_commit.author + end + + expect(recorder.count).to be_zero + end + end end describe '#to_reference' do @@ -182,7 +258,6 @@ eos it { is_expected.to respond_to(:date) } it { is_expected.to respond_to(:diffs) } it { is_expected.to respond_to(:id) } - it { is_expected.to respond_to(:to_patch) } end describe '#closes_issues' do diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 2ed29052dc1..f3f2bc28d2c 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -565,4 +565,10 @@ describe CommitStatus do it_behaves_like 'commit status enqueued' end end + + describe '#present' do + subject { commit_status.present } + + it { is_expected.to be_a(CommitStatusPresenter) } + end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 3d3092b8ac9..bd6bf5b0712 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -266,6 +266,19 @@ describe Issuable do end end + describe '#time_estimate=' do + it 'coerces the value below Gitlab::Database::MAX_INT_VALUE' do + expect { issue.time_estimate = 100 }.to change { issue.time_estimate }.to(100) + expect { issue.time_estimate = Gitlab::Database::MAX_INT_VALUE + 100 }.to change { issue.time_estimate }.to(Gitlab::Database::MAX_INT_VALUE) + end + + it 'skips coercion for not Integer values' do + expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil) + expect { issue.time_estimate = 'invalid time' }.not_to raise_error(StandardError) + expect { issue.time_estimate = 22.33 }.not_to raise_error(StandardError) + end + end + describe '#to_hook_data' do let(:builder) { double } diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb new file mode 100644 index 00000000000..b821a84d5e0 --- /dev/null +++ b/spec/models/concerns/sortable_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe Sortable do + describe '.order_by' do + let(:relation) { Group.all } + + describe 'ordering by id' do + it 'ascending' do + expect(relation).to receive(:reorder).with(id: :asc) + + relation.order_by('id_asc') + end + + it 'descending' do + expect(relation).to receive(:reorder).with(id: :desc) + + relation.order_by('id_desc') + end + end + + describe 'ordering by created day' do + it 'ascending' do + expect(relation).to receive(:reorder).with(created_at: :asc) + + relation.order_by('created_asc') + end + + it 'descending' do + expect(relation).to receive(:reorder).with(created_at: :desc) + + relation.order_by('created_desc') + end + + it 'order by "date"' do + expect(relation).to receive(:reorder).with(created_at: :desc) + + relation.order_by('created_date') + end + end + + describe 'ordering by name' do + it 'ascending' do + expect(relation).to receive(:reorder).with("lower(name) asc") + + relation.order_by('name_asc') + end + + it 'descending' do + expect(relation).to receive(:reorder).with("lower(name) desc") + + relation.order_by('name_desc') + end + end + + describe 'ordering by Updated Time' do + it 'ascending' do + expect(relation).to receive(:reorder).with(updated_at: :asc) + + relation.order_by('updated_asc') + end + + it 'descending' do + expect(relation).to receive(:reorder).with(updated_at: :desc) + + relation.order_by('updated_desc') + end + end + + it 'does not call reorder in case of unrecognized ordering' do + expect(relation).not_to receive(:reorder) + + relation.order_by('random_ordering') + end + end + + describe 'sorting groups' do + def ordered_group_names(order) + Group.all.order_by(order).map(&:name) + end + + let!(:ref_time) { Time.parse('2018-05-01 00:00:00') } + let!(:group1) { create(:group, name: 'aa', id: 1, created_at: ref_time - 15.seconds, updated_at: ref_time) } + let!(:group2) { create(:group, name: 'AAA', id: 2, created_at: ref_time - 10.seconds, updated_at: ref_time - 5.seconds) } + let!(:group3) { create(:group, name: 'BB', id: 3, created_at: ref_time - 5.seconds, updated_at: ref_time - 10.seconds) } + let!(:group4) { create(:group, name: 'bbb', id: 4, created_at: ref_time, updated_at: ref_time - 15.seconds) } + + it 'sorts groups by id' do + expect(ordered_group_names('id_asc')).to eq(%w(aa AAA BB bbb)) + expect(ordered_group_names('id_desc')).to eq(%w(bbb BB AAA aa)) + end + + it 'sorts groups by name via case-insentitive comparision' do + expect(ordered_group_names('name_asc')).to eq(%w(aa AAA BB bbb)) + expect(ordered_group_names('name_desc')).to eq(%w(bbb BB AAA aa)) + end + + it 'sorts groups by created_at' do + expect(ordered_group_names('created_asc')).to eq(%w(aa AAA BB bbb)) + expect(ordered_group_names('created_desc')).to eq(%w(bbb BB AAA aa)) + expect(ordered_group_names('created_date')).to eq(%w(bbb BB AAA aa)) + end + + it 'sorts groups by updated_at' do + expect(ordered_group_names('updated_asc')).to eq(%w(bbb BB AAA aa)) + expect(ordered_group_names('updated_desc')).to eq(%w(aa AAA BB bbb)) + end + end +end diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index 673049d1cc4..a3e68d2e646 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -78,4 +78,10 @@ describe GenericCommitStatus do it { is_expected.not_to be_nil } end end + + describe '#present' do + subject { generic_commit_status.present } + + it { is_expected.to be_a(GenericCommitStatusPresenter) } + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 0907d28d33b..f83b52e8975 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -15,7 +15,7 @@ describe Group do it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:labels).class_name('GroupLabel') } it { is_expected.to have_many(:variables).class_name('Ci::GroupVariable') } - it { is_expected.to have_many(:uploads).dependent(:destroy) } + it { is_expected.to have_many(:uploads) } it { is_expected.to have_one(:chat_team) } it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') } it { is_expected.to have_many(:badges).class_name('GroupBadge') } @@ -691,4 +691,12 @@ describe Group do end end end + + context 'with uploads' do + it_behaves_like 'model with mounted uploader', true do + let(:model_object) { create(:group, :with_avatar) } + let(:upload_attribute) { :avatar } + let(:uploader_class) { AttachmentUploader } + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5b452f17979..39625b559eb 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -76,7 +76,7 @@ describe Project do it { is_expected.to have_many(:project_group_links) } it { is_expected.to have_many(:notification_settings).dependent(:delete_all) } it { is_expected.to have_many(:forks).through(:forked_project_links) } - it { is_expected.to have_many(:uploads).dependent(:destroy) } + it { is_expected.to have_many(:uploads) } it { is_expected.to have_many(:pipeline_schedules) } it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:clusters) } @@ -3739,4 +3739,12 @@ describe Project do it { is_expected.to be_nil } end end + + context 'with uploads' do + it_behaves_like 'model with mounted uploader', true do + let(:model_object) { create(:project, :with_avatar) } + let(:upload_attribute) { :avatar } + let(:uploader_class) { AttachmentUploader } + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index ac8d9a32d4e..6bc148a1392 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -671,7 +671,7 @@ describe Repository do end end - describe "search_files_by_content" do + shared_examples "search_files_by_content" do let(:results) { repository.search_files_by_content('feature', 'master') } subject { results } @@ -718,7 +718,7 @@ describe Repository do end end - describe "search_files_by_name" do + shared_examples "search_files_by_name" do let(:results) { repository.search_files_by_name('files', 'master') } it 'returns result' do @@ -758,6 +758,16 @@ describe Repository do end end + describe 'with gitaly enabled' do + it_behaves_like 'search_files_by_content' + it_behaves_like 'search_files_by_name' + end + + describe 'with gitaly disabled', :disable_gitaly do + it_behaves_like 'search_files_by_content' + it_behaves_like 'search_files_by_name' + end + describe '#async_remove_remote' do before do masterrev = repository.find_branch('master').dereferenced_target diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index bb5308221f0..684fa030baf 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -39,7 +39,7 @@ describe User do it { is_expected.to have_many(:builds).dependent(:nullify) } it { is_expected.to have_many(:pipelines).dependent(:nullify) } it { is_expected.to have_many(:chat_names).dependent(:destroy) } - it { is_expected.to have_many(:uploads).dependent(:destroy) } + it { is_expected.to have_many(:uploads) } it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') } it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') } @@ -1223,6 +1223,24 @@ describe User do end end + describe '#accept_pending_invitations!' do + let(:user) { create(:user, email: 'user@email.com') } + let!(:project_member_invite) { create(:project_member, :invited, invite_email: user.email) } + let!(:group_member_invite) { create(:group_member, :invited, invite_email: user.email) } + let!(:external_project_member_invite) { create(:project_member, :invited, invite_email: 'external@email.com') } + let!(:external_group_member_invite) { create(:group_member, :invited, invite_email: 'external@email.com') } + + it 'accepts all the user members pending invitations and returns the accepted_members' do + accepted_members = user.accept_pending_invitations! + + expect(accepted_members).to match_array([project_member_invite, group_member_invite]) + expect(group_member_invite.reload).not_to be_invite + expect(project_member_invite.reload).not_to be_invite + expect(external_project_member_invite.reload).to be_invite + expect(external_group_member_invite.reload).to be_invite + end + end + describe '#all_emails' do let(:user) { create(:user) } @@ -1786,28 +1804,54 @@ describe User do end end - describe '#ci_authorized_runners' do + describe '#ci_owned_runners' do let(:user) { create(:user) } - let(:runner) { create(:ci_runner) } + let(:runner_1) { create(:ci_runner) } + let(:runner_2) { create(:ci_runner) } - before do - project.runners << runner - end - - context 'without any projects' do - let(:project) { create(:project) } + context 'without any projects nor groups' do + let!(:project) { create(:project, runners: [runner_1]) } + let!(:group) { create(:group) } it 'does not load' do - expect(user.ci_authorized_runners).to be_empty + expect(user.ci_owned_runners).to be_empty end end context 'with personal projects runners' do let(:namespace) { create(:namespace, owner: user) } - let(:project) { create(:project, namespace: namespace) } + let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) } it 'loads' do - expect(user.ci_authorized_runners).to contain_exactly(runner) + expect(user.ci_owned_runners).to contain_exactly(runner_1) + end + end + + context 'with personal group runner' do + let!(:project) { create(:project, runners: [runner_1]) } + let!(:group) do + create(:group, runners: [runner_2]).tap do |group| + group.add_owner(user) + end + end + + it 'loads' do + expect(user.ci_owned_runners).to contain_exactly(runner_2) + end + end + + context 'with personal project and group runner' do + let(:namespace) { create(:namespace, owner: user) } + let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) } + + let!(:group) do + create(:group, runners: [runner_2]).tap do |group| + group.add_owner(user) + end + end + + it 'loads' do + expect(user.ci_owned_runners).to contain_exactly(runner_1, runner_2) end end @@ -1818,7 +1862,7 @@ describe User do end it 'loads' do - expect(user.ci_authorized_runners).to contain_exactly(runner) + expect(user.ci_owned_runners).to contain_exactly(runner_1) end end @@ -1828,14 +1872,28 @@ describe User do end it 'does not load' do - expect(user.ci_authorized_runners).to be_empty + expect(user.ci_owned_runners).to be_empty end end end context 'with groups projects runners' do let(:group) { create(:group) } - let(:project) { create(:project, group: group) } + let!(:project) { create(:project, group: group, runners: [runner_1]) } + + def add_user(access) + group.add_user(user, access) + end + + it_behaves_like :member + end + + context 'with groups runners' do + let!(:group) do + create(:group, runners: [runner_1]).tap do |group| + group.add_owner(user) + end + end def add_user(access) group.add_user(user, access) @@ -1845,7 +1903,7 @@ describe User do end context 'with other projects runners' do - let(:project) { create(:project) } + let!(:project) { create(:project, runners: [runner_1]) } def add_user(access) project.add_role(user, access) @@ -1858,7 +1916,7 @@ describe User do let(:group) { create(:group) } let(:another_user) { create(:user) } let(:subgroup) { create(:group, parent: group) } - let(:project) { create(:project, group: subgroup) } + let!(:project) { create(:project, group: subgroup, runners: [runner_1]) } def add_user(access) group.add_user(user, access) @@ -2769,4 +2827,12 @@ describe User do expect { user.increment_failed_attempts! }.not_to change(user, :failed_attempts) end end + + context 'with uploads' do + it_behaves_like 'model with mounted uploader', false do + let(:model_object) { create(:user, :with_avatar) } + let(:upload_attribute) { :avatar } + let(:uploader_class) { AttachmentUploader } + end + end end diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index 4bc005df2fc..efd175247b5 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -10,7 +10,7 @@ describe Ci::BuildPresenter do end it 'inherits from Gitlab::View::Presenter::Delegated' do - expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated) + expect(described_class.ancestors).to include(Gitlab::View::Presenter::Delegated) end describe '#initialize' do diff --git a/spec/presenters/commit_status_presenter_spec.rb b/spec/presenters/commit_status_presenter_spec.rb new file mode 100644 index 00000000000..f81ee44e371 --- /dev/null +++ b/spec/presenters/commit_status_presenter_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe CommitStatusPresenter do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + + subject(:presenter) do + described_class.new(build) + end + + it 'inherits from Gitlab::View::Presenter::Delegated' do + expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated) + end +end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index bb0034e3237..7d923932309 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -738,13 +738,16 @@ describe API::Groups do describe "DELETE /groups/:id" do context "when authenticated as user" do it "removes group" do - delete api("/groups/#{group1.id}", user1) + Sidekiq::Testing.fake! do + expect { delete api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1) + end - expect(response).to have_gitlab_http_status(204) + expect(response).to have_gitlab_http_status(202) end it_behaves_like '412 response' do let(:request) { api("/groups/#{group1.id}", user1) } + let(:success_status) { 202 } end it "does not remove a group if not an owner" do @@ -773,7 +776,7 @@ describe API::Groups do it "removes any existing group" do delete api("/groups/#{group2.id}", admin) - expect(response).to have_gitlab_http_status(204) + expect(response).to have_gitlab_http_status(202) end it "does not remove a non existing group" do diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb new file mode 100644 index 00000000000..a55796cf343 --- /dev/null +++ b/spec/requests/api/markdown_spec.rb @@ -0,0 +1,112 @@ +require "spec_helper" + +describe API::Markdown do + RSpec::Matchers.define_negated_matcher :exclude, :include + + describe "POST /markdown" do + let(:user) {} # No-op. It gets overwritten in the contexts below. + + before do + post api("/markdown", user), params + end + + shared_examples "rendered markdown text without GFM" do + it "renders markdown text" do + expect(response).to have_http_status(201) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["html"]).to eq("<p>#{text}</p>") + end + end + + shared_examples "404 Project Not Found" do + it "responses with 404 Not Found" do + expect(response).to have_http_status(404) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["message"]).to eq("404 Project Not Found") + end + end + + context "when arguments are invalid" do + context "when text is missing" do + let(:params) { {} } + + it "responses with 400 Bad Request" do + expect(response).to have_http_status(400) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["error"]).to eq("text is missing") + end + end + + context "when project is not found" do + let(:params) { { text: "Hello world!", gfm: true, project: "Dummy project" } } + + it_behaves_like "404 Project Not Found" + end + end + + context "when arguments are valid" do + set(:project) { create(:project) } + set(:issue) { create(:issue, project: project) } + let(:text) { ":tada: Hello world! :100: #{issue.to_reference}" } + + context "when not using gfm" do + context "without project" do + let(:params) { { text: text } } + + it_behaves_like "rendered markdown text without GFM" + end + + context "with project" do + let(:params) { { text: text, project: project.full_path } } + + context "when not authorized" do + it_behaves_like "404 Project Not Found" + end + + context "when authorized" do + let(:user) { project.owner } + + it_behaves_like "rendered markdown text without GFM" + end + end + end + + context "when using gfm" do + context "without project" do + let(:params) { { text: text, gfm: true } } + + it "renders markdown text" do + expect(response).to have_http_status(201) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["html"]).to include("Hello world!") + .and include('data-name="tada"') + .and include('data-name="100"') + .and include("#1") + .and exclude("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"") + .and exclude("#1</a>") + end + end + + context "with project" do + let(:params) { { text: text, gfm: true, project: project.full_path } } + let(:user) { project.owner } + + it "renders markdown text" do + expect(response).to have_http_status(201) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["html"]).to include("Hello world!") + .and include('data-name="tada"') + .and include('data-name="100"') + .and include("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"") + .and include("#1</a>") + end + end + end + end + end +end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 981ac768e3a..c7587c877fc 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -27,7 +27,7 @@ describe API::Runners do end end - let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group]) } + let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group], runner_type: :group_type) } before do # Set project access for users @@ -48,7 +48,7 @@ describe API::Runners do expect(json_response).to be_an Array expect(json_response[0]).to have_key('ip_address') expect(descriptions).to contain_exactly( - 'Project runner', 'Two projects runner' + 'Project runner', 'Two projects runner', 'Group runner' ) expect(shared).to be_falsey end @@ -592,6 +592,15 @@ describe API::Runners do end.to change { project.runners.count }.by(+1) expect(response).to have_gitlab_http_status(201) end + + it 'enables a shared runner' do + expect do + post api("/projects/#{project.id}/runners", admin), runner_id: shared_runner.id + end.to change { project.runners.count }.by(1) + + expect(shared_runner.reload).not_to be_shared + expect(response).to have_gitlab_http_status(201) + end end context 'user is not admin' do diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb index a1cdf583de3..34d4b8e9565 100644 --- a/spec/requests/api/v3/groups_spec.rb +++ b/spec/requests/api/v3/groups_spec.rb @@ -458,9 +458,11 @@ describe API::V3::Groups do describe "DELETE /groups/:id" do context "when authenticated as user" do it "removes group" do - delete v3_api("/groups/#{group1.id}", user1) + Sidekiq::Testing.fake! do + expect { delete v3_api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1) + end - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(202) end it "does not remove a group if not an owner" do @@ -489,7 +491,7 @@ describe API::V3::Groups do it "removes any existing group" do delete v3_api("/groups/#{group2.id}", admin) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(202) end it "does not remove a non existing group" do diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index 2473c561f4b..e67d12b7a89 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -26,6 +26,13 @@ describe PipelineEntity do expect(subject).to include :updated_at, :created_at end + it 'excludes coverage data when disabled' do + entity = described_class + .represent(pipeline, request: request, disable_coverage: true) + + expect(entity.as_json).not_to include(:coverage) + end + it 'contains details' do expect(subject).to include :details expect(subject[:details]) diff --git a/spec/services/keys/destroy_service_spec.rb b/spec/services/keys/destroy_service_spec.rb new file mode 100644 index 00000000000..28ac72ddd42 --- /dev/null +++ b/spec/services/keys/destroy_service_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe Keys::DestroyService do + let(:user) { create(:user) } + + subject { described_class.new(user) } + + it 'destroys a key' do + key = create(:key) + + expect { subject.execute(key) }.to change(Key, :count).by(-1) + end +end diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb index be09afd9f36..723cb374c37 100644 --- a/spec/services/projects/update_remote_mirror_service_spec.rb +++ b/spec/services/projects/update_remote_mirror_service_spec.rb @@ -343,7 +343,11 @@ describe Projects::UpdateRemoteMirrorService do tag = repository.find_tag(name) target = tag.try(:target) target_commit = tag.try(:dereferenced_target) - tags << Gitlab::Git::Tag.new(repository.raw_repository, name, target, target_commit) + tags << Gitlab::Git::Tag.new(repository.raw_repository, { + name: name, + target: target, + target_commit: target_commit + }) end end diff --git a/spec/support/shared_examples/malicious_regexp_shared_examples.rb b/spec/support/shared_examples/malicious_regexp_shared_examples.rb index ac5d22298bb..65026f1d7c0 100644 --- a/spec/support/shared_examples/malicious_regexp_shared_examples.rb +++ b/spec/support/shared_examples/malicious_regexp_shared_examples.rb @@ -1,3 +1,5 @@ +require 'timeout' + shared_examples 'malicious regexp' do let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' } let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' } diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb new file mode 100644 index 00000000000..47ad0c6345d --- /dev/null +++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +shared_examples_for 'model with mounted uploader' do |supports_fileuploads| + describe '.destroy' do + before do + stub_uploads_object_storage(uploader_class) + + model_object.public_send(upload_attribute).migrate!(ObjectStorage::Store::REMOTE) + end + + it 'deletes remote uploads' do + expect_any_instance_of(CarrierWave::Storage::Fog::File).to receive(:delete).and_call_original + + expect { model_object.destroy }.to change { Upload.count }.by(-1) + end + + it 'deletes any FileUploader uploads which are not mounted', skip: !supports_fileuploads do + create(:upload, uploader: FileUploader, model: model_object) + + expect { model_object.destroy }.to change { Upload.count }.by(-2) + end + end +end |