diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index d06387c0f4d78baf6e7ccab4ac6e4949ae5ed8ac..3eddf595f30ebe53b0bf85dc465b251225b39de8 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -78,7 +78,7 @@ export default class BlobViewer { .fail(() => new Flash('Error loading source view')) .done((data) => { viewer.innerHTML = data.html; - $(viewer).syntaxHighlight(); + $(viewer).renderGFM(); viewer.setAttribute('data-loaded', 'true'); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index bf9fbf2ecb529d5b19daff5d9a7a493ebc0d73d6..44e16e52296300e4a13363f9825f712ca55893f8 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -124,7 +124,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); break; case 'projects:merge_requests:index': case 'projects:issues:index': - if (gl.FilteredSearchManager) { + if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) { const filteredSearchManager = new gl.FilteredSearchManager( page === 'projects:issues:index' ? 'issues' : 'merge_requests', ); diff --git a/app/assets/javascripts/issue_show/issue_title_description.vue b/app/assets/javascripts/issue_show/issue_title_description.vue index 8a7a813efd88c4d3310593ed739d6d6f2980f5ad..a8b1b99d166b9522e2d5d089a685e9adfdd8d7b0 100644 --- a/app/assets/javascripts/issue_show/issue_title_description.vue +++ b/app/assets/javascripts/issue_show/issue_title_description.vue @@ -4,6 +4,7 @@ import Poll from './../lib/utils/poll'; import Service from './services/index'; import tasks from './actions/tasks'; import edited from './components/edited.vue'; +import normalizeNewlines from '../lib/utils/normalize_newlines'; export default { props: { @@ -105,7 +106,7 @@ export default { this.title = title; this.description = description; - this.$nextTick(() => { + setTimeout(() => { this.updateFlag('titleFlag', false); this.updateFlag('descriptionFlag', false); }); @@ -119,7 +120,8 @@ export default { this.titleText = this.apiData.title_text; const noTitleChange = this.title === title; - const noDescriptionChange = this.description === description; + const noDescriptionChange = + normalizeNewlines(this.description) === normalizeNewlines(description); /** * since opacity is changed, even if there is no diff for Vue to update diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 2f682fbd2fbf8530835df57d1c792df225edae17..7e62773ae6c83e3ba326c77cd2ec531b5b2394ad 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -135,7 +135,10 @@ gl.utils.getUrlParamsArray = function () { // We can trust that each param has one & since values containing & will be encoded // Remove the first character of search as it is always ? - return window.location.search.slice(1).split('&'); + return window.location.search.slice(1).split('&').map((param) => { + const split = param.split('='); + return [decodeURI(split[0]), split[1]].join('='); + }); }; gl.utils.isMetaKey = function(e) { diff --git a/app/assets/javascripts/lib/utils/normalize_newlines.js b/app/assets/javascripts/lib/utils/normalize_newlines.js new file mode 100644 index 0000000000000000000000000000000000000000..f155c3f7587f436326b20ca1c83265e32e221316 --- /dev/null +++ b/app/assets/javascripts/lib/utils/normalize_newlines.js @@ -0,0 +1,5 @@ +function normalizeNewlines(str) { + return str.replace(/(\r|
)?(\n|
)/g, '\n'); +} + +export default normalizeNewlines; diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index 66f39122a663f4de230084e87fb5451f4dc6dfa7..973d6119158b94d576aaaaffda780d1324ef6a19 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -1,47 +1,48 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, max-len */ -(function() { - (function(w) { - var notificationGranted, notifyMe, notifyPermissions; - notificationGranted = function(message, opts, onclick) { - var notification; - notification = new Notification(message, opts); - setTimeout(function() { - return notification.close(); - // Hide the notification after X amount of seconds - }, 8000); - if (onclick) { - return notification.onclick = onclick; - } - }; - notifyPermissions = function() { - if ('Notification' in window) { - return Notification.requestPermission(); - } - }; - notifyMe = function(message, body, icon, onclick) { - var opts; - opts = { - body: body, - icon: icon - }; - // Let's check if the browser supports notifications - if (!('Notification' in window)) { +function notificationGranted(message, opts, onclick) { + var notification; + notification = new Notification(message, opts); + setTimeout(function() { + // Hide the notification after X amount of seconds + return notification.close(); + }, 8000); + + return notification.onclick = onclick || notification.close; +} - // do nothing - } else if (Notification.permission === 'granted') { - // If it's okay let's create a notification +function notifyPermissions() { + if ('Notification' in window) { + return Notification.requestPermission(); + } +} + +function notifyMe(message, body, icon, onclick) { + var opts; + opts = { + body: body, + icon: icon + }; + // Let's check if the browser supports notifications + if (!('Notification' in window)) { + // do nothing + } else if (Notification.permission === 'granted') { + // If it's okay let's create a notification + return notificationGranted(message, opts, onclick); + } else if (Notification.permission !== 'denied') { + return Notification.requestPermission(function(permission) { + // If the user accepts, let's create a notification + if (permission === 'granted') { return notificationGranted(message, opts, onclick); - } else if (Notification.permission !== 'denied') { - return Notification.requestPermission(function(permission) { - // If the user accepts, let's create a notification - if (permission === 'granted') { - return notificationGranted(message, opts, onclick); - } - }); } - }; - w.notify = notifyMe; - return w.notifyPermissions = notifyPermissions; - })(window); -}).call(window); + }); + } +} + +const notify = { + notificationGranted, + notifyPermissions, + notifyMe, +}; + +export default notify; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index abacab9b4c31de3571fc7c8f53daa2728b203b07..3932df5f9932db0cdab6cc85c55f170a162fd11b 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -56,7 +56,6 @@ import './lib/utils/animate'; import './lib/utils/bootstrap_linked_tabs'; import './lib/utils/common_utils'; import './lib/utils/datetime_utility'; -import './lib/utils/notify'; import './lib/utils/pretty_time'; import './lib/utils/text_utility'; import './lib/utils/type_utility'; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index c709730f78f72b9d3eb9380472e442f7b7bf6242..e40d6572b18ab9bfe590e4254abfa2dd5ac40449 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -285,7 +285,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // Similar to `toggler_behavior` in the discussion tab const hash = window.gl.utils.getLocationHash(); const anchor = hash && $container.find(`[id="${hash}"]`); - if (anchor) { + if (anchor && anchor.length > 0) { const notesContent = anchor.closest('.notes_content'); const lineType = notesContent.hasClass('new') ? 'new' : 'old'; notes.toggleDiffNote({ diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 9fbf72c044ecbc23bae77509fded759125ffa6a2..31f14614179184fa30d9f145ee77fb2508615e4c 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -7,6 +7,7 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; import CommentTypeToggle from './comment_type_toggle'; +import normalizeNewlines from './lib/utils/normalize_newlines'; require('./autosave'); window.autosize = require('vendor/autosize'); @@ -17,10 +18,6 @@ require('vendor/jquery.caret'); // required by jquery.atwho require('vendor/jquery.atwho'); require('./task_list'); -const normalizeNewlines = function(str) { - return str.replace(/\r\n/g, '\n'); -}; - (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index e9619f915538db8a49923d272323ccb1034f2c40..b5a6dc159bb65d73c852bf1391ad219c5c473832 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -13,7 +13,7 @@ export default { }, data() { return { - removeSourceBranch: true, + removeSourceBranch: this.mr.shouldRemoveSourceBranch, mergeWhenBuildSucceeds: false, useCommitMessageWithDescription: false, setToMergeWhenPipelineSucceeds: false, @@ -70,6 +70,9 @@ export default { || this.isApprovalNeeded || this.mr.preventMerge); }, + isRemoveSourceBranchButtonDisabled() { + return this.isMergeButtonDisabled || !this.mr.canRemoveSourceBranch; + }, shouldShowSquashBeforeMerge() { const { commitsCount, enableSquashBeforeMerge } = this.mr; return enableSquashBeforeMerge && commitsCount > 1; @@ -256,8 +259,9 @@ export default { <template v-if="isMergeAllowed()"> <label class="spacing"> <input + id="remove-source-branch-input" v-model="removeSourceBranch" - :disabled="isMergeButtonDisabled" + :disabled="isRemoveSourceBranchButtonDisabled" type="checkbox"/> Remove source branch </label> diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index 35e9a5aec1c78ced20ddb24072293ce79336a526..7bd5a9a786dc47e0e682e7be0e9ab3c0ce17980f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -30,3 +30,4 @@ export { default as getStateKey } from './ee/stores/get_state_key'; export { default as mrWidgetOptions } from './ee/mr_widget_options'; export { default as stateMaps } from './ee/stores/state_maps'; export { default as SquashBeforeMerge } from './ee/components/states/mr_widget_squash_before_merge'; +export { default as notify } from '../lib/utils/notify'; diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index cd65ac069c5c4324498606826c503f4dac8665de..43ef468c303a578b3da50934ec8c9bd81ad2bcdf 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -4,6 +4,8 @@ import { } from './dependencies'; document.addEventListener('DOMContentLoaded', () => { + gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; + const vm = new Vue(mrWidgetOptions); window.gl.mrWidget = { diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 2d218925c066d55eea256cd79756e576a8037b64..4ba6bd7b4fabf22fd90119126fd1a31b76e86f75 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -29,6 +29,7 @@ import { eventHub, stateMaps, SquashBeforeMerge, + notify, } from './dependencies'; export default { @@ -79,8 +80,10 @@ export default { this.service.checkStatus() .then(res => res.json()) .then((res) => { + this.handleNotification(res); this.mr.setData(res); this.setFavicon(); + if (cb) { cb.call(null, res); } @@ -138,6 +141,15 @@ export default { new Flash('Something went wrong. Please try again.'); // eslint-disable-line }); }, + handleNotification(data) { + if (data.ci_status === this.mr.ciStatus) return; + + const label = data.pipeline.details.status.label; + const title = `Pipeline ${label}`; + const message = `Pipeline ${label} for "${data.title}"`; + + notify.notifyMe(title, message, this.mr.gitlabLogo); + }, resumePolling() { this.pollingInterval.resume(); }, diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index cde519e57bc0f5822b7dc600cff1b4ab1b19d9e4..eb214e768a7a1de85b8919b51e99c2e814f8c704 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -5,6 +5,8 @@ export default class MergeRequestStore { constructor(data) { this.sha = data.diff_head_sha; + this.gitlabLogo = data.gitlabLogo; + this.setData(data); } @@ -53,7 +55,7 @@ export default class MergeRequestStore { this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path; this.removeWIPPath = data.remove_wip_path; this.sourceBranchRemoved = !data.source_branch_exists; - this.shouldRemoveSourceBranch = (data.merge_params || {}).should_remove_source_branch || false; + this.shouldRemoveSourceBranch = data.remove_source_branch || false; this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false; this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false; this.mergePath = data.merge_path; diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 0db3ac1a60e0aeead785a6e14dd4ad90fba7b428..75907c35b7e900fcc35c6328412c35fa9980ccf5 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -10,7 +10,7 @@ top: 0; margin-top: 3px; padding: $gl-padding; - z-index: 9; + z-index: 300; width: 300px; font-size: 14px; background-color: $white-light; @@ -110,6 +110,7 @@ .award-control { margin: 0 5px 6px 0; outline: 0; + position: relative; &.disabled { cursor: default; @@ -227,8 +228,8 @@ .award-control-icon-positive, .award-control-icon-super-positive { position: absolute; - left: 11px; - bottom: 7px; + left: 10px; + bottom: 6px; opacity: 0; @include transition(opacity, transform); } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index e489870a3ce9a62fd394fb22fa38f6ee46e37d00..96effc38335d6afcf53c27d8a7d56f8f3054cec9 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -479,4 +479,5 @@ .filter-dropdown-loading { padding: 8px 16px; + text-align: center; } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 70db19622282c2b02b2a171c8e21026a2b40425c..ddccfc96819a3ba1d90081a0c2a4bfbbda6f12cf 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -3,12 +3,6 @@ margin: 0; padding: 0; - .note-text { - p:last-child { - margin-bottom: 0; - } - } - .system-note { .note-text { color: $gl-text-color !important; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 14a62b6cbf005de0db5083e6fc9de29e02f1fe34..47aa7a1d658be1332a8b55368e8f4809cb6b11a0 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -390,6 +390,10 @@ .container-fluid.container-limited { max-width: 100%; } + + .content-wrapper { + padding-bottom: 6px; + } } .build-detail-row { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index e52b58906e16c4c775ac01165aeb65ab03774292..f33e3f8416d0148c93ebe282ea954504da85f306 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -69,6 +69,10 @@ } } + .btn .text-center { + display: inline; + } + .commit-title { margin: 0; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index ddb04b147090b02e524ce691271b410d58c0b16f..2735c5bd98656d1cf8138705acd08a62553d49db 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -510,17 +510,13 @@ position: absolute; border-top: 2px solid $border-color; height: 1px; - top: 8px; + top: 9px; width: 8px; left: 0; } &:last-child { margin-bottom: 0; - - &::before { - top: 14px; - } } } @@ -529,7 +525,7 @@ width: 2px; background: $border-color; position: absolute; - top: -5px; + top: -9px; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 99bcf612e8fcfa55ddcc11cc300df3f52af2da2a..d513ee7eb0a806296a3826211b04fb356d28334e 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -394,6 +394,12 @@ ul.notes { padding-bottom: 8px; } +.note-header-author-name { + @media (max-width: $screen-xs-max) { + display: none; + } +} + .note-headline-light { display: inline; @@ -665,7 +671,7 @@ ul.notes { .line-resolve-all { vertical-align: middle; display: inline-block; - padding: 6px 10px; + padding: 5px 10px 6px; background-color: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; @@ -678,6 +684,10 @@ ul.notes { .line-resolve-btn { margin-right: 5px; + + svg { + vertical-align: middle; + } } } @@ -714,6 +724,10 @@ ul.notes { } } +.line-resolve-text { + vertical-align: middle; +} + .discussion-next-btn { svg { margin: 0; @@ -731,9 +745,8 @@ ul.notes { // Merge request notes in diffs .diff-file { // Diff is side by side - .notes_content.parallel .note-header .note-headline-light { + .notes_content.parallel .note-header .note-header-author-name { display: block; - position: relative; } // Diff is inline .notes_content .note-header .note-headline-light { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 041a5f5d85af88633351e893358f3e1fe062de3d..ce3a909d26b3ca4b0d688b41975ff83eaa924541 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -98,6 +98,10 @@ } } + .btn .text-center { + display: inline; + } + .tooltip { white-space: nowrap; } diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index d0a692070d93ec5ed04c833abd987f934d8e225a..b68d76aeff0591eba73d9833f6f68789770bf38b 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -17,10 +17,18 @@ def mark_as_spam private + def ensure_spam_config_loaded! + return @spam_config_loaded if defined?(@spam_config_loaded) + + @spam_config_loaded = Gitlab::Recaptcha.load_configurations! + end + def recaptcha_check_with_fallback(&fallback) if spammable.valid? redirect_to spammable elsif render_recaptcha? + ensure_spam_config_loaded! + if params[:recaptcha_verification] flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' end @@ -35,7 +43,7 @@ def spammable_params default_params = { request: request } recaptcha_check = params[:recaptcha_verification] && - Gitlab::Recaptcha.load_configurations! && + ensure_spam_config_loaded! && verify_recaptcha return default_params unless recaptcha_check diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index f68610e197cc6d6545520940bace1bc313bcae3a..03d145c4ccd22bc22e79e41fc69495fb01216e3a 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -16,7 +16,10 @@ def execute def all_groups groups = [] - groups << current_user.authorized_groups if current_user + if current_user + groups << Group.member_self_and_descendants(current_user.id) + groups << current_user.groups_through_project_authorizations + end groups << Group.unscoped.public_to_user(current_user) groups diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index e5b1e6e8bc70d0eba506444c26e9af2ad717affc..4e6e68059204c1a9f05f7b7a27d91315ec2d32d0 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -69,13 +69,12 @@ def destroy_label_path(label) end def render_colored_label(label, label_suffix = '', tooltip: true) - label_color = label.color || Label::DEFAULT_COLOR - text_color = text_color_for_bg(label_color) + text_color = text_color_for_bg(label.color) # Intentionally not using content_tag here so that this method can be called # by LabelReferenceFilter span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) + - %(style="background-color: #{label_color}; color: #{text_color}" ) + + %(style="background-color: #{label.color}; color: #{text_color}" ) + %(title="#{escape_once(label.description)}" data-container="body">) + %(#{escape_once(label.name)}#{label_suffix}</span>) diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index b739554a7a42c5099717cb74c75c5af2f3dbd087..6d3b1ca435882b5312914201e46513a6a69f992e 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -9,6 +9,7 @@ def submodule_links(submodule_item, ref = nil, repository = @repository) if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/ namespace, project = $1, $2 + project.rstrip! project.sub!(/\.git\z/, '') if self_url?(url, namespace, project) diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 6d7cc83971e002df176b452daa20b86c3891f5bb..07213ca608a956b70ee44e9ede192975f93de559 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -10,9 +10,9 @@ class PipelineSchedule < ActiveRecord::Base has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline' has_many :pipelines - validates :cron, unless: :importing_or_inactive?, cron: true, presence: { unless: :importing_or_inactive? } - validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? } - validates :ref, presence: { unless: :importing_or_inactive? } + validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? } + validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? } + validates :ref, presence: { unless: :importing? } validates :description, presence: true before_save :set_next_run_at @@ -28,8 +28,12 @@ def inactive? !active? end - def importing_or_inactive? - importing? || inactive? + def deactivate! + update_attribute(:active, false) + end + + def runnable_by_owner? + Ability.allowed?(owner, :create_pipeline, project) end def set_next_run_at diff --git a/app/models/label.rb b/app/models/label.rb index ddddb6bdf8fa60471c77b295b8bd8755cdfc8dd0..074239702f82b9ea4a1a51493f4abc615bde93f1 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -133,6 +133,10 @@ def template? template end + def color + super || DEFAULT_COLOR + end + def text_color LabelsHelper.text_color_for_bg(self.color) end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 7c5ceb28d1cd10559c56346121e204a4f5a15581..c6d0b0c70f0c7aa5934a8d783db7fcee216c0b08 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -109,7 +109,7 @@ def self.upcoming_ids_by_projects(projects) end def participants - User.joins(assigned_issues: :milestone).where("milestones.id = ?", id) + User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq end def self.sort(method) diff --git a/app/models/repository.rb b/app/models/repository.rb index d4124465e9aec971382965c7a4484a67a4895cdf..6baafc9137365e567336d861745298471ab347dc 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1027,6 +1027,8 @@ def merge_base(first_commit_id, second_commit_id) end def is_ancestor?(ancestor_id, descendant_id) + return false if ancestor_id.nil? || descendant_id.nil? + Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| if is_enabled raw_repository.is_ancestor?(ancestor_id, descendant_id) diff --git a/app/models/user.rb b/app/models/user.rb index 2c7a6499abd89e6ecec5e0f6434671c6edc3a12c..0e6593781ea8d8c33f9604521359af897d79ee04 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -519,6 +519,17 @@ def authorized_groups Group.where("namespaces.id IN (#{union.to_sql})") end + def groups_through_project_authorizations + projects = Project.joins(:project_authorizations). + where('project_authorizations.user_id = ?', id ). + joins(:route). + select('routes.path AS full_path') + + Group.joins(:route). + joins("INNER JOIN (#{projects.to_sql}) project_paths + ON project_paths.full_path LIKE CONCAT(routes_namespaces.path, '/%')") + end + def nested_groups Group.member_descendants(id) end @@ -934,13 +945,13 @@ def global_notification_setting end def assigned_open_merge_requests_count(force: false) - Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do + Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count end end def assigned_open_issues_count(force: false) - Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do + Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count end end @@ -951,10 +962,18 @@ def update_cache_counts end def invalidate_cache_counts - Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count']) + invalidate_issue_cache_counts + invalidate_merge_request_cache_counts + end + + def invalidate_issue_cache_counts Rails.cache.delete(['users', id, 'assigned_open_issues_count']) end + def invalidate_merge_request_cache_counts + Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count']) + end + def todos_done_count(force: false) Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do TodosFinder.new(self, state: :done).execute.count diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index d4af4490608622bf64494f489249d5f21bb858ee..2d7405dc2403446061246f110fceeaa71e98b831 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -23,7 +23,7 @@ def protected_action? !::Gitlab::UserAccess .new(user, project: build.project) - .can_push_to_branch?(build.ref) + .can_merge_to_branch?(build.ref) end end end diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb index 054d681f131de57c7445729659deb44fd0ec6935..8a6ef7a720e92d59fab1e52615c109439f22bdd1 100644 --- a/app/serializers/merge_request_entity.rb +++ b/app/serializers/merge_request_entity.rb @@ -58,6 +58,7 @@ class MergeRequestEntity < IssuableEntity expose :commits_count expose :cannot_be_merged?, as: :has_conflicts expose :can_be_merged?, as: :can_be_merged + expose :remove_source_branch?, as: :remove_source_branch expose :project_archived do |merge_request| merge_request.project.archived? diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index f70edb982cd2437038e1955c320e019583f98f8b..d1b202930e652714dec01f7d147831cce32cfc8b 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -178,7 +178,7 @@ def create(issuable) after_create(issuable) issuable.create_cross_references!(current_user) execute_hooks(issuable) - issuable.assignees.each(&:invalidate_cache_counts) + invalidate_cache_counts(issuable.assignees, issuable) end issuable @@ -239,6 +239,7 @@ def update(issuable) new_assignees = issuable.assignees.to_a affected_assignees = (old_assignees + new_assignees) - (old_assignees & new_assignees) affected_assignees.compact.each(&:invalidate_cache_counts) + invalidate_cache_counts(affected_assignees.compact, issuable) end after_update(issuable) @@ -331,4 +332,10 @@ def handle_common_system_notes(issuable, old_labels: []) create_labels_note(issuable, old_labels) if issuable.labels != old_labels end + + def invalidate_cache_counts(users, issuable) + users.each do |user| + user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") + end + end end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index f1030912c68750e348df38be54803eaa73735963..85c616ca576d56cf63961e5c37efe66f9d94c0bb 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -28,6 +28,7 @@ def close_issue(issue, commit: nil, notifications: true, system_note: true) notification_service.close_issue(issue, current_user) if notifications todo_service.close_issue(issue, current_user) execute_hooks(issue, 'close') + invalidate_cache_counts(issue.assignees, issue) end issue diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb index 40fbe354492842ecc67d26710c3b717f0e7c946c..80ea6312768bc9c662806938a0d14d702fc19004 100644 --- a/app/services/issues/reopen_service.rb +++ b/app/services/issues/reopen_service.rb @@ -8,6 +8,7 @@ def execute(issue) create_note(issue) notification_service.reopen_issue(issue, current_user) execute_hooks(issue, 'reopen') + invalidate_cache_counts(issue.assignees, issue) end issue diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index f2053bda83aadbb4dbcb8766999f404d02b6c7bc..2ffc989ed71c984d40445c408caf374c8b2ca465 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -13,6 +13,7 @@ def execute(merge_request, commit = nil) notification_service.close_mr(merge_request, current_user) todo_service.close_merge_request(merge_request, current_user) execute_hooks(merge_request, 'close') + invalidate_cache_counts(merge_request.assignees, merge_request) end merge_request diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index e8fb1b597527cb1088988cbefff626bec2cdff94..f0d998731d75be214ac3eb1dc098d35857e5fc04 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -13,6 +13,7 @@ def execute(merge_request) create_note(merge_request) notification_service.merge_mr(merge_request, current_user) execute_hooks(merge_request, 'merge') + invalidate_cache_counts(merge_request.assignees, merge_request) end private diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb index fadcce5d9b6e13f27e68d8a3433cc471fc1a6eea..b016e5235cd079e0921e9c7220afe146bf96e113 100644 --- a/app/services/merge_requests/reopen_service.rb +++ b/app/services/merge_requests/reopen_service.rb @@ -10,6 +10,7 @@ def execute(merge_request) execute_hooks(merge_request, 'reopen') merge_request.reload_diff merge_request.mark_as_unchecked + invalidate_cache_counts(merge_request.assignees, merge_request) end merge_request diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 2c239e123ab191cba71f0bd54c6dc58879b4dfa2..ccee5e4b6ce341abf6bfb4d54a009967e4e5dc82 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -92,26 +92,20 @@ def extractor desc 'Assign' explanation do |users| - "Assigns #{users.map(&:to_reference).to_sentence}." if users.any? + "Assigns #{users.first.to_reference}." if users.any? end params '@user' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) end parse_params do |assignee_param| - users = extract_references(assignee_param, :user) - - if users.empty? - users = User.where(username: assignee_param.split(' ').map(&:strip)) - end - - users + extract_users(assignee_param) end command :assign do |users| next if users.empty? if issuable.is_a?(Issue) - @updates[:assignee_ids] = users.map(&:id) + @updates[:assignee_ids] = [users.last.id] else @updates[:assignee_id] = users.last.id end @@ -487,6 +481,18 @@ def extractor end end + def extract_users(params) + return [] if params.nil? + + users = extract_references(params, :user) + + if users.empty? + users = User.where(username: params.split(' ').map(&:strip)) + end + + users + end + def find_labels(labels_param) extract_references(labels_param, :label) | LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb index 95a891111e117e50e0526130aaca935d3d32b826..02589959c2f2ff62066b954c5c6022f25ad820f2 100644 --- a/app/uploaders/lfs_object_uploader.rb +++ b/app/uploaders/lfs_object_uploader.rb @@ -12,4 +12,20 @@ def cache_dir def filename model.oid[4..-1] end + + def work_dir + File.join(Gitlab.config.lfs.storage_path, 'tmp', 'work') + end + + private + + # To prevent LFS files from moving across filesystems, override the default + # implementation: + # http://github.com/carrierwaveuploader/carrierwave/blob/v1.0.0/lib/carrierwave/uploader/cache.rb#L181-L183 + def workfile_path(for_file = original_filename) + # To be safe, keep this directory outside of the the cache directory + # because calling CarrierWave.clean_cache_files! will remove any files in + # the cache directory. + File.join(work_dir, @cache_id, version_name.to_s, for_file) + end end diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index 506246f2ee60826b3b79693e9c8bb8a97a271330..e2baaa625aef4dcd4beacd333affc4c1deda5bc9 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -8,6 +8,7 @@ = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |action| + - next unless can?(current_user, :update_build, action) %li = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do = custom_icon('icon_play') diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 7315e6710569b103e6f72180c751f557d374d6c3..9e221240cf2141d74a8e56023bee0e59b3c74690 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -13,7 +13,7 @@ = render 'projects/environments/metrics_button', environment: @environment - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' - - if can?(current_user, :create_deployment, @environment) && @environment.can_stop? + - if can?(current_user, :stop_environment, @environment) = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post .environments-container diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 075ecee434364b2c254575b14fa51adb49fd8388..9e4f96839f4909c112b00903459bdb9a92ba9a45 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -4,7 +4,9 @@ = pipeline_schedule.description %td.branch-name-cell = icon('code-fork') - = link_to pipeline_schedule.ref, namespace_project_commits_path(@project.namespace, @project, pipeline_schedule.ref), class: "branch-name" + - if pipeline_schedule.ref + = link_to pipeline_schedule.ref, namespace_project_commits_path(@project.namespace, @project, pipeline_schedule.ref), class: "branch-name" + %td - if pipeline_schedule.last_pipeline .status-icon-container{ class: "ci-status-icon-#{pipeline_schedule.last_pipeline.status}" } @@ -15,7 +17,7 @@ None %td.next-run-cell - if pipeline_schedule.active? - = time_ago_with_tooltip(pipeline_schedule.next_run_at) + = time_ago_with_tooltip(pipeline_schedule.real_next_run) - else Inactive %td diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml index 41c09012e668cecc49f20efb6a5f1ba2ddfd6b5d..d40ab6b3dd4c25201bf34836b63f8a341d614b3e 100644 --- a/app/views/shared/issuable/form/_merge_params.html.haml +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -3,14 +3,14 @@ - return unless issuable.is_a?(MergeRequest) - return if issuable.closed_without_fork? --# This check is duplicated below, to avoid conflicts with EE. -- return unless issuable.can_remove_source_branch?(current_user) - .form-group .col-sm-10.col-sm-offset-2 - .checkbox - = label_tag 'merge_request[squash]' do - = hidden_field_tag 'merge_request[squash]', '0', id: nil - = check_box_tag 'merge_request[squash]', '1', issuable.squash - Squash commits when merge request is accepted. - = link_to 'About this feature', help_page_path('user/project/merge_requests/squash_and_merge') + - if issuable.can_remove_source_branch?(current_user) + .checkbox + - initial_checkbox_value = issuable.merge_params.key?('force_remove_source_branch') ? issuable.force_remove_source_branch? : true + = label_tag 'merge_request[force_remove_source_branch]' do + = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil + = check_box_tag 'merge_request[force_remove_source_branch]', '1', initial_checkbox_value + Remove source branch when merge request is accepted. + += render 'shared/issuable/form/ee/squash_merge_param', issuable: issuable diff --git a/app/views/shared/issuable/form/ee/_squash_merge_param.html.haml b/app/views/shared/issuable/form/ee/_squash_merge_param.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..7cce11752df55fd8abc193ea8008ea4066d4e951 --- /dev/null +++ b/app/views/shared/issuable/form/ee/_squash_merge_param.html.haml @@ -0,0 +1,8 @@ +.form-group + .col-sm-10.col-sm-offset-2 + .checkbox + = label_tag 'merge_request[squash]' do + = hidden_field_tag 'merge_request[squash]', '0', id: nil + = check_box_tag 'merge_request[squash]', '1', issuable.squash + Squash commits when merge request is accepted. + = link_to 'About this feature', help_page_path('user/project/merge_requests/squash_and_merge') diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index a7bf610b9c709e41089766c0b3094a9c7569ccea..1e34b7c1e76acd2bef84930de58a4e8a2403ff5e 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -18,7 +18,7 @@ .note-header .note-header-info %a{ href: user_path(note.author) } - %span.hidden-xs + %span.note-header-author-name = sanitize(note.author.name) %span.note-headline-light = note.author.to_reference diff --git a/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml b/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml new file mode 100644 index 0000000000000000000000000000000000000000..4137050a0771eaca8375ebbc4d063f11fbe2e23a --- /dev/null +++ b/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml @@ -0,0 +1,4 @@ +--- +title: Fix the last coverage in trace log should be extracted +merge_request: 11128 +author: dosuken123 diff --git a/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml b/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml new file mode 100644 index 0000000000000000000000000000000000000000..a58f3a7429e36e412ba74c9169a4166470a7f74c --- /dev/null +++ b/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml @@ -0,0 +1,4 @@ +--- +title: Fix pipeline_schedules pages throwing error 500 +merge_request: 11706 +author: dosuken123 diff --git a/changelogs/unreleased/32995-issue-contents-dynamically-replaced-with-stale-version-after-saving-or-refreshing-relative-external_url-only.yml b/changelogs/unreleased/32995-issue-contents-dynamically-replaced-with-stale-version-after-saving-or-refreshing-relative-external_url-only.yml new file mode 100644 index 0000000000000000000000000000000000000000..5cd36a4e3e2ab675c992b9265146369221f352bf --- /dev/null +++ b/changelogs/unreleased/32995-issue-contents-dynamically-replaced-with-stale-version-after-saving-or-refreshing-relative-external_url-only.yml @@ -0,0 +1,4 @@ +--- +title: Fix incorrect ETag cache key when relative instance URL is used +merge_request: 11964 +author: diff --git a/changelogs/unreleased/33048-markdown-rendering-of-md-files-has-ceased-to-display-latex-equations.yml b/changelogs/unreleased/33048-markdown-rendering-of-md-files-has-ceased-to-display-latex-equations.yml new file mode 100644 index 0000000000000000000000000000000000000000..5648e013e75e72db4031a62ccf9360ec673d1860 --- /dev/null +++ b/changelogs/unreleased/33048-markdown-rendering-of-md-files-has-ceased-to-display-latex-equations.yml @@ -0,0 +1,4 @@ +--- +title: Fix math rendering on blob pages +merge_request: +author: diff --git a/changelogs/unreleased/counters_cache_invalidation.yml b/changelogs/unreleased/counters_cache_invalidation.yml new file mode 100644 index 0000000000000000000000000000000000000000..1e78765ec101378ac2a9e3ef5b921215ccec5172 --- /dev/null +++ b/changelogs/unreleased/counters_cache_invalidation.yml @@ -0,0 +1,4 @@ +--- +title: Invalidate cache for issue and MR counters more granularly +merge_request: +author: diff --git a/changelogs/unreleased/fix-backup-restore-resume.yml b/changelogs/unreleased/fix-backup-restore-resume.yml new file mode 100644 index 0000000000000000000000000000000000000000..b7dfd451f5dea3ce65ad986e9210ca5d48bb5cb4 --- /dev/null +++ b/changelogs/unreleased/fix-backup-restore-resume.yml @@ -0,0 +1,4 @@ +--- +title: Make backup task to continue on corrupt repositories +merge_request: 11962 +author: diff --git a/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml b/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml new file mode 100644 index 0000000000000000000000000000000000000000..43c18502cd6e60734ce8622fa1537055860e0655 --- /dev/null +++ b/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml @@ -0,0 +1,4 @@ +--- +title: Respect merge, instead of push, permissions for protected actions +merge_request: 11648 +author: diff --git a/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml b/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml new file mode 100644 index 0000000000000000000000000000000000000000..161bce456017baff7605fd2e011a9614b777056e --- /dev/null +++ b/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml @@ -0,0 +1,4 @@ +--- +title: Fix LFS timeouts when trying to save large files +merge_request: +author: diff --git a/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml b/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml new file mode 100644 index 0000000000000000000000000000000000000000..d633995d4676eb0706fe6c63654d1e962ef77783 --- /dev/null +++ b/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml @@ -0,0 +1,4 @@ +--- +title: Strip trailing whitespaces in submodule URLs +merge_request: +author: diff --git a/changelogs/unreleased/sh-recaptcha-fix-try2.yml b/changelogs/unreleased/sh-recaptcha-fix-try2.yml new file mode 100644 index 0000000000000000000000000000000000000000..94729252c6f59c33ab080007f78f2772806b8e85 --- /dev/null +++ b/changelogs/unreleased/sh-recaptcha-fix-try2.yml @@ -0,0 +1,4 @@ +--- +title: Make sure reCAPTCHA configuration is loaded when spam checks are initiated +merge_request: +author: diff --git a/changelogs/unreleased/zj-drop-fk-if-exists.yml b/changelogs/unreleased/zj-drop-fk-if-exists.yml new file mode 100644 index 0000000000000000000000000000000000000000..237ba936de98c2c13a4bdcac59f31e6541e5086d --- /dev/null +++ b/changelogs/unreleased/zj-drop-fk-if-exists.yml @@ -0,0 +1,4 @@ +--- +title: Remove foreigh key on ci_trigger_schedules only if it exists +merge_request: +author: diff --git a/db/migrate/20170425112628_remove_foreigh_key_ci_trigger_schedules.rb b/db/migrate/20170425112628_remove_foreigh_key_ci_trigger_schedules.rb index 6116ca59ee4d10e1453c59963951997c75455ab8..1587eee06ae2f8f38bc7c89dc0bdcb02cb8fa068 100644 --- a/db/migrate/20170425112628_remove_foreigh_key_ci_trigger_schedules.rb +++ b/db/migrate/20170425112628_remove_foreigh_key_ci_trigger_schedules.rb @@ -4,10 +4,20 @@ class RemoveForeighKeyCiTriggerSchedules < ActiveRecord::Migration DOWNTIME = false def up - remove_foreign_key :ci_trigger_schedules, column: :trigger_id + if fk_on_trigger_schedules? + remove_foreign_key :ci_trigger_schedules, column: :trigger_id + end end def down # no op, the foreign key should not have been here end + + private + + # Not made more generic and lifted to the helpers as Rails 5 will provide + # such an API + def fk_on_trigger_schedules? + connection.foreign_keys(:ci_trigger_schedules).include?("ci_triggers") + end end diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index e542b1119eab18309f9ee1f46775e50b224765ac..0a0be717fa8308b93077153b4d3063db7712b8f3 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -587,7 +587,7 @@ Optional manual actions have `allow_failure: true` set by default. **Manual actions are considered to be write actions, so permissions for protected branches are used when user wants to trigger an action. In other words, in order to trigger a manual action assigned to a branch that the -pipeline is running for, user needs to have ability to push to this branch.** +pipeline is running for, user needs to have ability to merge to this branch.** ### environment diff --git a/features/project/merge_requests/accept.feature b/features/project/merge_requests/accept.feature index c45ed9ea68b3d378a1d1a8deafce7b62da5d5a73..2ab1c19f4527b82c96611b6c19f32a3a8d5199fa 100644 --- a/features/project/merge_requests/accept.feature +++ b/features/project/merge_requests/accept.feature @@ -7,6 +7,7 @@ Feature: Project Merge Requests Acceptance @javascript Scenario: Accepting the Merge Request and removing the source branch Given I am on the Merge Request detail page + When I check the "Remove source branch" option And I click on Accept Merge Request Then I should see merge request merged And I should not see the Remove Source Branch button @@ -14,6 +15,7 @@ Feature: Project Merge Requests Acceptance @javascript Scenario: Accepting the Merge Request when URL has an anchor Given I am on the Merge Request detail with note anchor page + When I check the "Remove source branch" option And I click on Accept Merge Request Then I should see merge request merged And I should not see the Remove Source Branch button @@ -21,7 +23,6 @@ Feature: Project Merge Requests Acceptance @javascript Scenario: Accepting the Merge Request without removing the source branch Given I am on the Merge Request detail page - When I click on "Remove source branch" option When I click on Accept Merge Request Then I should see merge request merged And I should see the Remove Source Branch button diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb index 3c976f675a26e0dce00a6b6c83343a64ee3126d8..cdaa849308c126541a7080443f15fe1e43e740e2 100644 --- a/features/steps/project/merge_requests/acceptance.rb +++ b/features/steps/project/merge_requests/acceptance.rb @@ -11,10 +11,14 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps visit merge_request_path(@merge_request, anchor: 'note_123') end - step 'I click on "Remove source branch" option' do + step 'I uncheck the "Remove source branch" option' do uncheck('Remove source branch') end + step 'I check the "Remove source branch" option' do + check('Remove source branch') + end + step 'I click on Accept Merge Request' do click_button('Merge') end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 6b29600a75133a3d350c7028e18161f6853b6eaf..a1685c779167bf288cc055da796e0ec030e996c8 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -7,15 +7,15 @@ def dump prepare Project.find_each(batch_size: 1000) do |project| - $progress.print " * #{project.path_with_namespace} ... " + progress.print " * #{project.path_with_namespace} ... " path_to_project_repo = path_to_repo(project) path_to_project_bundle = path_to_bundle(project) # Create namespace dir if missing FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.full_path)) if project.namespace - if project.empty_repo? - $progress.puts "[SKIPPED]".color(:cyan) + if empty_repo?(project) + progress.puts "[SKIPPED]".color(:cyan) else in_path(path_to_project_repo) do |dir| FileUtils.mkdir_p(path_to_tars(project)) @@ -23,10 +23,7 @@ def dump output, status = Gitlab::Popen.popen(cmd) unless status.zero? - puts "[FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Backup failed' + progress_warn(project, cmd.join(' '), output) end end @@ -34,12 +31,9 @@ def dump output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts "[DONE]".color(:green) + progress.puts "[DONE]".color(:green) else - puts "[FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Backup failed' + progress_warn(project, cmd.join(' '), output) end end @@ -48,19 +42,16 @@ def dump path_to_wiki_bundle = path_to_bundle(wiki) if File.exist?(path_to_wiki_repo) - $progress.print " * #{wiki.path_with_namespace} ... " - if wiki.repository.empty? - $progress.puts " [SKIPPED]".color(:cyan) + progress.print " * #{wiki.path_with_namespace} ... " + if empty_repo?(wiki) + progress.puts " [SKIPPED]".color(:cyan) else cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_wiki_repo} bundle create #{path_to_wiki_bundle} --all) output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts " [DONE]".color(:green) + progress.puts " [DONE]".color(:green) else - puts " [FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Backup failed' + progress_warn(wiki, cmd.join(' '), output) end end end @@ -80,7 +71,7 @@ def restore end Project.find_each(batch_size: 1000) do |project| - $progress.print " * #{project.path_with_namespace} ... " + progress.print " * #{project.path_with_namespace} ... " path_to_project_repo = path_to_repo(project) path_to_project_bundle = path_to_bundle(project) @@ -94,12 +85,9 @@ def restore output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts "[DONE]".color(:green) + progress.puts "[DONE]".color(:green) else - puts "[FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Restore failed' + progress_warn(project, cmd.join(' '), output) end in_path(path_to_tars(project)) do |dir| @@ -107,10 +95,7 @@ def restore output, status = Gitlab::Popen.popen(cmd) unless status.zero? - puts "[FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Restore failed' + progress_warn(project, cmd.join(' '), output) end end @@ -119,7 +104,7 @@ def restore path_to_wiki_bundle = path_to_bundle(wiki) if File.exist?(path_to_wiki_bundle) - $progress.print " * #{wiki.path_with_namespace} ... " + progress.print " * #{wiki.path_with_namespace} ... " # If a wiki bundle exists, first remove the empty repo # that was initialized with ProjectWiki.new() and then @@ -129,22 +114,19 @@ def restore output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts " [DONE]".color(:green) + progress.puts " [DONE]".color(:green) else - puts " [FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Restore failed' + progress_warn(project, cmd.join(' '), output) end end end - $progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow) + progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow) cmd = %W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts " [DONE]".color(:green) + progress.puts " [DONE]".color(:green) else puts " [FAILED]".color(:red) puts "failed: #{cmd}" @@ -201,8 +183,25 @@ def silent private + def progress_warn(project, cmd, output) + progress.puts "[WARNING] Executing #{cmd}".color(:orange) + progress.puts "Ignoring error on #{project.path_with_namespace} - #{output}".color(:orange) + end + + def empty_repo?(project_or_wiki) + project_or_wiki.repository.empty_repo? + rescue => e + progress.puts "Ignoring repository error and continuing backing up project: #{project_or_wiki.path_with_namespace} - #{e.message}".color(:orange) + + false + end + def repository_storage_paths_args Gitlab.config.repositories.storages.values.map { |rs| rs['path'] } end + + def progress + $progress + end end end diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index fa462cbe095054545a1ccd7fde7de5cab1087727..c4c0623df6c559df9e761da009dee6dd77662a5d 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -73,7 +73,7 @@ def extract_coverage(regex) match = "" - stream.each_line do |line| + reverse_line do |line| matches = line.scan(regex) next unless matches.is_a?(Array) next if matches.empty? @@ -86,34 +86,39 @@ def extract_coverage(regex) nil rescue # if bad regex or something goes wrong we dont want to interrupt transition - # so we just silentrly ignore error for now + # so we just silently ignore error for now end private - def read_last_lines(last_lines) - chunks = [] - pos = lines = 0 - max = stream.size - - # We want an extra line to make sure fist line has full contents - while lines <= last_lines && pos < max - pos += BUFFER_SIZE - - buf = - if pos <= max - stream.seek(-pos, IO::SEEK_END) - stream.read(BUFFER_SIZE) - else # Reached the head, read only left - stream.seek(0) - stream.read(BUFFER_SIZE - (pos - max)) - end - - lines += buf.count("\n") - chunks.unshift(buf) + def read_last_lines(limit) + to_enum(:reverse_line).first(limit).reverse.join + end + + def reverse_line + stream.seek(0, IO::SEEK_END) + debris = '' + + until (buf = read_backward(BUFFER_SIZE)).empty? + buf += debris + debris, *lines = buf.each_line.to_a + lines.reverse_each do |line| + yield(line.force_encoding('UTF-8')) + end end - chunks.join.lines.last(last_lines).join + yield(debris.force_encoding('UTF-8')) unless debris.empty? + end + + def read_backward(length) + cur_offset = stream.tell + start = cur_offset - length + start = 0 if start < 0 + + stream.seek(start, IO::SEEK_SET) + stream.read(cur_offset - start).tap do + stream.seek(start, IO::SEEK_SET) + end end end end diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb index 270d67dd50c2e25ed17fd6cf495f9ae8645ec61a..7f884183bb1b387c7ea506086660d510f0b31d08 100644 --- a/lib/gitlab/etag_caching/middleware.rb +++ b/lib/gitlab/etag_caching/middleware.rb @@ -6,12 +6,13 @@ def initialize(app) end def call(env) - route = Gitlab::EtagCaching::Router.match(env) + request = Rack::Request.new(env) + route = Gitlab::EtagCaching::Router.match(request) return @app.call(env) unless route track_event(:etag_caching_middleware_used, route) - etag, cached_value_present = get_etag(env) + etag, cached_value_present = get_etag(request) if_none_match = env['HTTP_IF_NONE_MATCH'] if if_none_match == etag @@ -27,8 +28,8 @@ def call(env) private - def get_etag(env) - cache_key = env['PATH_INFO'] + def get_etag(request) + cache_key = request.path store = Gitlab::EtagCaching::Store.new current_value = store.get(cache_key) cached_value_present = current_value.present? diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb index d74e31af5c676fe8f5d1d9f5fe27d399309ebfe2..597ccb58bfca55673a12fe94455b26a69fe17c1b 100644 --- a/lib/gitlab/etag_caching/router.rb +++ b/lib/gitlab/etag_caching/router.rb @@ -43,8 +43,8 @@ class Router ), ].freeze - def self.match(env) - ROUTES.find { |route| route.regexp.match(env['PATH_INFO']) } + def self.match(request) + ROUTES.find { |route| route.regexp.match(request.path_info) } end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 44684f36c451733a0f8ff8797d39344f423a1d19..e7935086b4bbef86b97a4bd6ad93ea5fb4934ace 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -1,3 +1,5 @@ +# rubocop:disable Metrics/AbcSize + module Gitlab module GonHelper def add_gon_variables @@ -14,6 +16,7 @@ def add_gon_variables gon.gitlab_url = Gitlab.config.gitlab.url gon.test = Rails.env.test? gon.revision = Gitlab::REVISION + gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png') if current_user gon.current_user_id = current_user.id diff --git a/lib/gitlab/slash_commands/command_definition.rb b/lib/gitlab/slash_commands/command_definition.rb index 12a385f90fdf539da695e93b80367505803a4564..caab88560145fd8b71c1a978bf921cca4236773b 100644 --- a/lib/gitlab/slash_commands/command_definition.rb +++ b/lib/gitlab/slash_commands/command_definition.rb @@ -48,17 +48,23 @@ def execute(context, opts, arg) end def to_h(opts) + context = OpenStruct.new(opts) + desc = description if desc.respond_to?(:call) - context = OpenStruct.new(opts) desc = context.instance_exec(&desc) rescue '' end + prms = params + if prms.respond_to?(:call) + prms = Array(context.instance_exec(&prms)) rescue params + end + { name: name, aliases: aliases, description: desc, - params: params + params: prms } end diff --git a/lib/gitlab/slash_commands/dsl.rb b/lib/gitlab/slash_commands/dsl.rb index 614bafbe1b2139e3ea14fab0ad30ffcdb009c853..1b5b4566d8123c864b448e4dd38e5fd4f9301620 100644 --- a/lib/gitlab/slash_commands/dsl.rb +++ b/lib/gitlab/slash_commands/dsl.rb @@ -40,8 +40,8 @@ def desc(text = '', &block) # command :command_key do |arguments| # # Awesome code block # end - def params(*params) - @params = params + def params(*params, &block) + @params = block_given? ? block : params end # Allows to give an explanation of what the command will do when diff --git a/qa/Dockerfile b/qa/Dockerfile index 72c82503542ad951325a141e713cebf9dfaddee7..9e2a74ef991ec3844011d7188bedc2a5d02daddc 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,10 +1,25 @@ FROM ruby:2.3 LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>" -RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \ - apt-get update && apt-get install -y --force-yes \ - libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \ - apt-get clean +## +# Update APT sources and install some dependencies +# +RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list +RUN apt-get update && apt-get install -y wget git unzip xvfb + +## +# At this point Google Chrome Beta is 59 - first version with headless support +# +RUN wget -q https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb +RUN dpkg -i google-chrome-beta_current_amd64.deb; apt-get -fy install + +## +# Install chromedriver to make it work with Selenium +# +RUN wget -q https://chromedriver.storage.googleapis.com/2.29/chromedriver_linux64.zip +RUN unzip chromedriver_linux64.zip -d /usr/local/bin + +RUN apt-get clean WORKDIR /home/qa diff --git a/qa/Gemfile b/qa/Gemfile index 6bfe25ba4371b6372bb7743b47f7269fdc923c80..5d089a459346357e978500c46929713c073c5cad 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -2,6 +2,6 @@ source 'https://rubygems.org' gem 'capybara', '~> 2.12.1' gem 'capybara-screenshot', '~> 1.0.14' -gem 'capybara-webkit', '~> 1.12.0' gem 'rake', '~> 12.0.0' gem 'rspec', '~> 3.5' +gem 'selenium-webdriver', '~> 2.53' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 6de2abff1985fa60949cd279bcc08f5d6488f7b6..4dd71aa50106972e5013b2355f2ecfea7ecfe13b 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -16,7 +16,10 @@ GEM capybara-webkit (1.12.0) capybara (>= 2.3.0, < 2.13.0) json + childprocess (0.7.0) + ffi (~> 1.0, >= 1.0.11) diff-lcs (1.3) + ffi (1.9.18) json (2.0.3) launchy (2.4.3) addressable (~> 2.3) @@ -44,6 +47,12 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.5.0) rspec-support (3.5.0) + rubyzip (1.2.1) + selenium-webdriver (2.53.4) + childprocess (~> 0.5) + rubyzip (~> 1.0) + websocket (~> 1.0) + websocket (1.2.4) xpath (2.0.0) nokogiri (~> 1.3) @@ -56,6 +65,7 @@ DEPENDENCIES capybara-webkit (~> 1.12.0) rake (~> 12.0.0) rspec (~> 3.5) + selenium-webdriver (~> 2.53) BUNDLED WITH 1.14.6 diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb index d72187fcd34ff750f417e51d752a0949207a1e88..78a93828d36b17c706befdc0f6ab90740eb6976d 100644 --- a/qa/qa/specs/config.rb +++ b/qa/qa/specs/config.rb @@ -1,7 +1,7 @@ require 'rspec/core' require 'capybara/rspec' -require 'capybara-webkit' require 'capybara-screenshot/rspec' +require 'selenium-webdriver' # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/LineLength @@ -20,7 +20,6 @@ def perform configure_rspec! configure_capybara! - configure_webkit! end def configure_rspec! @@ -43,9 +42,9 @@ def configure_rspec! config.order = :random Kernel.srand config.seed - config.before(:all) do - page.current_window.resize_to(1200, 1800) - end + # config.before(:all) do + # page.current_window.resize_to(1200, 1800) + # end config.formatter = :documentation config.color = true @@ -53,26 +52,28 @@ def configure_rspec! end def configure_capybara! + Capybara.register_driver :chrome do |app| + capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( + 'chromeOptions' => { + 'binary' => '/opt/google/chrome-beta/google-chrome-beta', + 'args' => %w[headless no-sandbox disable-gpu] + } + ) + + Capybara::Selenium::Driver + .new(app, browser: :chrome, desired_capabilities: capabilities) + end + Capybara.configure do |config| config.app_host = @address - config.default_driver = :webkit - config.javascript_driver = :webkit + config.default_driver = :chrome + config.javascript_driver = :chrome config.default_max_wait_time = 4 # https://github.com/mattheworiordan/capybara-screenshot/issues/164 config.save_path = 'tmp' end end - - def configure_webkit! - Capybara::Webkit.configure do |config| - config.allow_url(@address) - config.block_unknown_urls - end - rescue RuntimeError # rubocop:disable Lint/HandleExceptions - # TODO, Webkit is already configured, this make this - # configuration step idempotent, should be improved. - end end end end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index c07a32346734b883388acd66fd0aff64a301d5a2..64d06ef65580fe31e139787931e7d8155b254502 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -12,7 +12,6 @@ config.shared_context_metadata_behavior = :apply_to_host_groups config.disable_monkey_patching! config.expose_dsl_globally = true - config.warnings = true config.profile_examples = 10 config.order = :random Kernel.srand config.seed diff --git a/scripts/static-analysis b/scripts/static-analysis index 7dc8f6790364041d58291c05df26c943758f2ca5..6d35684b97ff10181b879fa3b2c850da7985bab3 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -3,7 +3,7 @@ require ::File.expand_path('../lib/gitlab/popen', __dir__) tasks = [ - %w[bundle exec bundle-audit check --update --ignore CVE-2016-4658], + %w[bundle exec bundle-audit check --update --ignore CVE-2016-4658 CVE-2017-5029], %w[bundle exec rake config_lint], %w[bundle exec rake flay], %w[bundle exec rake haml_lint], diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index da0a0c1e7f03d4c4a5f930e017b656c54e96640e..86333ae9bb7f2d3b707b0e505d754c027a567167 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -2,7 +2,7 @@ describe GroupsController do let(:user) { create(:user) } - let(:group) { create(:group) } + let(:group) { create(:group, :public) } let(:project) { create(:empty_project, namespace: group) } let!(:group_member) { create(:group_member, group: group, user: user) } @@ -35,14 +35,15 @@ sign_in(user) end - it 'shows the public subgroups' do + it 'shows all subgroups' do get :subgroups, id: group.to_param - expect(assigns(:nested_groups)).to contain_exactly(public_subgroup) + expect(assigns(:nested_groups)).to contain_exactly(public_subgroup, private_subgroup) end - context 'being member' do + context 'being member of private subgroup' do it 'shows public and private subgroups the user is member of' do + group_member.destroy! private_subgroup.add_guest(user) get :subgroups, id: group.to_param diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/builds_controller_spec.rb index 3ce23c17cdc684d133c65410c632d0030b1d3255..025b845de30b66c8c60a07334bfd416f175ade2c 100644 --- a/spec/controllers/projects/builds_controller_spec.rb +++ b/spec/controllers/projects/builds_controller_spec.rb @@ -261,7 +261,11 @@ def post_retry describe 'POST play' do before do - project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: 'master', project: project) + sign_in(user) post_play diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 1f79e72495ac7c0c8f5b50cc567fad28d034f5f7..60a327c43c0642a7f7d0f1f931a1430fccddb482 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -234,6 +234,7 @@ def update_spam_issue before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) } it 'rejects an issue recognized as a spam' do + expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true) expect { update_spam_issue }.not_to change{ issue.reload.title } end diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb index b054d62a76600e73599b1c251bc6780b610ad5b0..8c145535039a612bfd379026458a31b8a5cf1f9d 100644 --- a/spec/features/merge_requests/edit_mr_spec.rb +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -27,6 +27,19 @@ expect(page).to have_content 'Someone edited the merge request the same time you did' end + it 'allows to unselect "Remove source branch"', js: true do + merge_request.update(merge_params: { 'force_remove_source_branch' => '1' }) + expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy + + visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) + uncheck 'Remove source branch when merge request is accepted' + + click_button 'Save changes' + + expect(page).to have_unchecked_field 'remove-source-branch-input' + expect(page).to have_content 'Remove source branch' + end + it 'should preserve description textarea height', js: true do long_description = %q( Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat. diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb index b33d7f90a31097f99fc380461b35283f68a4fef7..4e524393b26fdf6f10b19b687b870fe6aec66e48 100644 --- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb @@ -7,7 +7,8 @@ let(:merge_request) do create(:merge_request_with_diffs, source_project: project, author: user, - title: 'Bug NS-04') + title: 'Bug NS-04', + merge_params: { force_remove_source_branch: '1' }) end let(:pipeline) do @@ -38,7 +39,7 @@ click_button "Merge when pipeline succeeds" expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds." - expect(page).to have_content "The source branch will be removed." + expect(page).to have_content "The source branch will not be removed." expect(page).to have_selector ".js-cancel-auto-merge" visit_merge_request(merge_request) # Needed to refresh the page expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i @@ -79,7 +80,8 @@ source_project: project, title: 'Bug NS-04', author: user, - merge_user: user) + merge_user: user, + merge_params: { force_remove_source_branch: '1' }) end before do @@ -96,7 +98,7 @@ click_link 'Merge when pipeline succeeds' expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds." - expect(page).to have_content "The source branch will be removed." + expect(page).to have_content "The source branch will not be removed." expect(page).to have_link "Cancel automatic merge" end end diff --git a/spec/features/merge_requests/squash_spec.rb b/spec/features/merge_requests/squash_spec.rb index 1803b027b82f6d1b40df3bdae9e15113539493e0..d32a40ce87eea8f8870b6ff642cf7590d5bd76d2 100644 --- a/spec/features/merge_requests/squash_spec.rb +++ b/spec/features/merge_requests/squash_spec.rb @@ -45,6 +45,9 @@ def accept_mr end before do + # Prevent source branch from being removed so we can use be_merged_to_root_ref + # method to check if squash was performed or not + allow_any_instance_of(MergeRequest).to receive(:force_remove_source_branch?).and_return(false) project.team << [user, :master] login_as user diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index 8dd69d193cb03ca0e6e6158f4fcac71cceeab9e3..862eaf724a0595059e18f14c4a11f8ca99dd30f4 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -220,4 +220,25 @@ end end end + + context 'user can merge into source project but cannot push to fork', js: true do + let(:fork_project) { create(:project, :public) } + let(:user2) { create(:user) } + + before do + project.team << [user2, :master] + logout + login_as user2 + merge_request.update(target_project: fork_project) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'user can merge into the source project' do + expect(page).to have_button('Merge', disabled: false) + end + + it 'user cannot remove source branch' do + expect(page).to have_field('remove-source-branch-input', disabled: true) + end + end end diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 86ce50c976f0a7591e33651216ae6362d37219e6..18b608c863efe50bdfe0516692986d6d86af835e 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -12,6 +12,7 @@ feature 'environment details page' do given!(:environment) { create(:environment, project: project) } + given!(:permissions) { } given!(:deployment) { } given!(:action) { } @@ -62,20 +63,31 @@ name: 'deploy to production') end - given(:role) { :master } + context 'when user has ability to trigger deployment' do + given(:permissions) do + create(:protected_branch, :developers_can_merge, + name: action.ref, project: project) + end - scenario 'does show a play button' do - expect(page).to have_link(action.name.humanize) - end + it 'does show a play button' do + expect(page).to have_link(action.name.humanize) + end + + it 'does allow to play manual action' do + expect(action).to be_manual - scenario 'does allow to play manual action' do - expect(action).to be_manual + expect { click_link(action.name.humanize) } + .not_to change { Ci::Pipeline.count } - expect { click_link(action.name.humanize) } - .not_to change { Ci::Pipeline.count } + expect(page).to have_content(action.name) + expect(action.reload).to be_pending + end + end - expect(page).to have_content(action.name) - expect(action.reload).to be_pending + context 'when user has no ability to trigger a deployment' do + it 'does not show a play button' do + expect(page).not_to have_link(action.name.humanize) + end end context 'with external_url' do @@ -134,12 +146,23 @@ on_stop: 'close_app') end - given(:role) { :master } + context 'when user has ability to stop environment' do + given(:permissions) do + create(:protected_branch, :developers_can_merge, + name: action.ref, project: project) + end - scenario 'does allow to stop environment' do - click_link('Stop') + it 'allows to stop environment' do + click_link('Stop') - expect(page).to have_content('close_app') + expect(page).to have_content('close_app') + end + end + + context 'when user has no ability to stop environment' do + it 'does not allow to stop environment' do + expect(page).to have_no_link('Stop') + end end context 'for reporter' do @@ -150,12 +173,6 @@ end end end - - context 'without stop action' do - scenario 'does allow to stop environment' do - click_link('Stop') - end - end end context 'when environment is stopped' do diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index 1211b17b3d8de56548baa67eea65354c1def14b1..39b4dd7235ba93657e4b5234fca8ee09ff1a8bb6 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -5,7 +5,7 @@ include WaitForAjax let!(:project) { create(:project) } - let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project ) } let!(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) } let(:scope) { nil } let!(:user) { create(:user) } @@ -32,6 +32,7 @@ it 'displays the required information description' do page.within('.pipeline-schedule-table-row') do expect(page).to have_content('pipeline schedule') + expect(page).to have_content(pipeline_schedule.real_next_run.strftime('%b %d, %Y')) expect(page).to have_link('master') expect(page).to have_link("##{pipeline.id}") end @@ -65,6 +66,17 @@ expect(page).not_to have_content('pipeline schedule') end end + + context 'when ref is nil' do + before do + pipeline_schedule.update_attribute(:ref, nil) + visit_pipelines_schedules + end + + it 'shows a list of the pipeline schedules with empty ref column' do + expect(first('.branch-name-cell').text).to eq('') + end + end end describe 'POST /projects/pipeline_schedules/new', js: true do @@ -108,6 +120,19 @@ expect(page).to have_content('my brand new description') end + + context 'when ref is nil' do + before do + pipeline_schedule.update_attribute(:ref, nil) + edit_pipeline_schedule + end + + it 'shows the pipeline schedule with default ref' do + page.within('.git-revision-dropdown-toggle') do + expect(first('.dropdown-toggle-text').text).to eq('master') + end + end + end end def visit_new_pipeline_schedule diff --git a/spec/features/projects/sub_group_issuables_spec.rb b/spec/features/projects/sub_group_issuables_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..cf21b208f65e951482607fe3fbbbe28cda7c85b4 --- /dev/null +++ b/spec/features/projects/sub_group_issuables_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'Subgroup Issuables', :feature, :js do + let!(:group) { create(:group, name: 'group') } + let!(:subgroup) { create(:group, parent: group, name: 'subgroup') } + let!(:project) { create(:empty_project, namespace: subgroup, name: 'project') } + let(:user) { create(:user) } + + before do + project.add_master(user) + login_as user + end + + it 'shows the full subgroup title when issues index page is empty' do + visit namespace_project_issues_path(project.namespace.to_param, project.to_param) + + expect_to_have_full_subgroup_title + end + + it 'shows the full subgroup title when merge requests index page is empty' do + visit namespace_project_merge_requests_path(project.namespace.to_param, project.to_param) + + expect_to_have_full_subgroup_title + end + + def expect_to_have_full_subgroup_title + title = find('.title-container') + + expect(title).not_to have_selector '.initializing' + expect(title).to have_content 'group / subgroup / project' + end +end diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb index 5b3591550c130b19f42658266f606a46931c1ace..e082631ce685f27b411761690d6d2d6e68003bb6 100644 --- a/spec/finders/groups_finder_spec.rb +++ b/spec/finders/groups_finder_spec.rb @@ -51,15 +51,48 @@ end context 'with a user' do + subject { described_class.new(user, parent: parent_group).execute } + it 'returns public and internal subgroups' do - expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup) + is_expected.to contain_exactly(public_subgroup, internal_subgroup) end context 'being member' do it 'returns public subgroups, internal subgroups, and private subgroups user is member of' do private_subgroup.add_guest(user) - expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup, private_subgroup) + is_expected.to contain_exactly(public_subgroup, internal_subgroup, private_subgroup) + end + end + + context 'parent group private' do + before do + parent_group.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + end + + context 'being member of parent group' do + it 'returns all subgroups' do + parent_group.add_guest(user) + + is_expected.to contain_exactly(public_subgroup, internal_subgroup, private_subgroup) + end + end + + context 'authorized to private project' do + it 'returns the subgroup of the project' do + subproject = create(:empty_project, :private, namespace: private_subgroup) + subproject.add_guest(user) + + is_expected.to include(private_subgroup) + end + + it 'returns all the parent groups if project is several levels deep' do + private_subsubgroup = create(:group, :private, parent: private_subgroup) + subsubproject = create(:empty_project, :private, namespace: private_subsubgroup) + subsubproject.add_guest(user) + + expect(described_class.new(user).execute).to include(private_subsubgroup, private_subgroup, parent_group) + end end end end diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json index 8e1820c933eb1fb6cbde1296e0e6ebc2979556e3..1a0cceb1cafa12aa04233267069de7cca4c26749 100644 --- a/spec/fixtures/api/schemas/entities/merge_request.json +++ b/spec/fixtures/api/schemas/entities/merge_request.json @@ -92,6 +92,8 @@ "diverged_commits_count": { "type": "integer" }, "commit_change_content_path": { "type": "string" }, "remove_wip_path": { "type": "string" }, + "commits_count": { "type": "integer" }, + "remove_source_branch": { "type": ["boolean", "null"] }, // EE-specific "rebase_commit_sha": { "type": ["string", "null"] }, "approvals_before_merge": { "type": ["integer", "null"] }, diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index 9da3379265957da223353da196f6b77547881c08..fa9945f3d73f590d4b8829f7e0d7749fb7555e01 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -102,6 +102,11 @@ expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash']) end + it 'handles urls with trailing whitespace' do + stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git ') + expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash']) + end + it 'returns original with non-standard url' do stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git') expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 47d904b865bf43550bc83a87df524fa93f3b3e34..a746a77654887a978b25c4b26f621a97c1656508 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -16,6 +16,16 @@ sha: merge_request.diff_head_sha ) end + let(:path) { "files/ruby/popen.rb" } + let(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: 14, + diff_refs: merge_request.diff_refs + ) + end render_views @@ -39,6 +49,12 @@ render_merge_request(example.description, merged_merge_request) end + it 'merge_requests/diff_comment.html.raw' do |example| + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) + render_merge_request(example.description, merge_request) + end + private def render_merge_request(fixture_file_name, merge_request) diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 5eb147ed888063a83224e184fd6bae18085b3de8..42a9067ade5ea8f5889cb1d459c8dbaa07134c84 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -41,6 +41,16 @@ require('~/lib/utils/common_utils'); const paramsArray = gl.utils.getUrlParamsArray(); expect(paramsArray[0][0] !== '?').toBe(true); }); + + it('should decode params', () => { + history.pushState('', '', '?label_name%5B%5D=test'); + + expect( + gl.utils.getUrlParamsArray()[0], + ).toBe('label_name[]=test'); + + history.pushState('', '', '?'); + }); }); describe('gl.utils.handleLocationHash', () => { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index e437333d5221b23f3779fbc49b773238a7371b1f..d119fc0c11e3fecb55af3ebbbf19a284c167a7fd 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,4 +1,5 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ +/* global Notes */ require('~/merge_request_tabs'); require('~/commit/pipelines/pipelines_bundle.js'); @@ -7,6 +8,7 @@ require('~/lib/utils/common_utils'); require('~/diff'); require('~/single_file_diff'); require('~/files_comment_button'); +require('~/notes'); require('vendor/jquery.scrollTo'); (function () { @@ -29,7 +31,7 @@ require('vendor/jquery.scrollTo'); }; $.extend(stubLocation, defaults, stubs || {}); }; - preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw', 'merge_requests/diff_comment.html.raw'); beforeEach(function () { this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); @@ -286,8 +288,49 @@ require('vendor/jquery.scrollTo'); spyOn($, 'ajax').and.callFake(function (options) { expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json'); }); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); }); + + describe('with note fragment hash', () => { + beforeEach(() => { + loadFixtures('merge_requests/diff_comment.html.raw'); + spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests'); + window.notes = new Notes('', []); + spyOn(window.notes, 'toggleDiffNote').and.callThrough(); + }); + + afterEach(() => { + delete window.notes; + }); + + it('should expand and scroll to linked fragment hash #note_xxx', function () { + const noteId = 'note_1'; + spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ html: `<div id="${noteId}">foo</div>` }); + }); + + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({ + target: jasmine.any(Object), + lineType: 'old', + forceShow: true, + }); + }); + + it('should gracefully ignore non-existant fragment hash', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ html: '' }); + }); + + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); + }); + }); }); }); }).call(window); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index d043ad38b8bd771ce44cd3337b7d206b94242eef..732b516badd301a9c038fe1c5019dbf52f2eb175 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -5,7 +5,7 @@ import * as simplePoll from '~/lib/utils/simple_poll'; const commitMessage = 'This is the commit message'; const commitMessageWithDescription = 'This is the commit message description'; -const createComponent = () => { +const createComponent = (customConfig = {}) => { const Component = Vue.extend(readyToMergeComponent); const mr = { isPipelineActive: false, @@ -17,8 +17,12 @@ const createComponent = () => { sha: '12345678', commitMessage, commitMessageWithDescription, + shouldRemoveSourceBranch: true, + canRemoveSourceBranch: false, }; + Object.assign(mr, customConfig.mr); + const service = { merge() {}, poll() {}, @@ -51,7 +55,6 @@ describe('MRWidgetReadyToMerge', () => { describe('data', () => { it('should have default data', () => { - expect(vm.removeSourceBranch).toBeTruthy(true); expect(vm.mergeWhenBuildSucceeds).toBeFalsy(); expect(vm.useCommitMessageWithDescription).toBeFalsy(); expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy(); @@ -166,6 +169,36 @@ describe('MRWidgetReadyToMerge', () => { expect(vm.isMergeButtonDisabled).toBeTruthy(); }); }); + + describe('Remove source branch checkbox', () => { + describe('when user can merge but cannot delete branch', () => { + it('isRemoveSourceBranchButtonDisabled should be true', () => { + expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true); + }); + + it('should be disabled in the rendered output', () => { + const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBe('disabled'); + }); + }); + + describe('when user can merge and can delete branch', () => { + beforeEach(() => { + this.customVm = createComponent({ + mr: { canRemoveSourceBranch: true }, + }); + }); + + it('isRemoveSourceBranchButtonDisabled should be false', () => { + expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false); + }); + + it('should be enabled in rendered output', () => { + const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBeNull(); + }); + }); + }); }); describe('methods', () => { diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index bdc18243a15413ded59472298ff15cc7fa07d389..3a0c50b750ffeb91a54746005e100cbe40992427 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options'; import eventHub from '~/vue_merge_request_widget/event_hub'; +import notify from '~/lib/utils/notify'; import mockData from './mock_data'; const createComponent = () => { @@ -107,6 +108,8 @@ describe('mrWidgetOptions', () => { it('should tell service to check status', (done) => { spyOn(vm.service, 'checkStatus').and.returnValue(returnPromise(mockData)); spyOn(vm.mr, 'setData'); + spyOn(vm, 'handleNotification'); + let isCbExecuted = false; const cb = () => { isCbExecuted = true; @@ -117,6 +120,7 @@ describe('mrWidgetOptions', () => { setTimeout(() => { expect(vm.service.checkStatus).toHaveBeenCalled(); expect(vm.mr.setData).toHaveBeenCalled(); + expect(vm.handleNotification).toHaveBeenCalledWith(mockData); expect(isCbExecuted).toBeTruthy(); done(); }, 333); @@ -254,6 +258,39 @@ describe('mrWidgetOptions', () => { }); }); + describe('handleNotification', () => { + const data = { + ci_status: 'running', + title: 'title', + pipeline: { details: { status: { label: 'running-label' } } }, + }; + + beforeEach(() => { + spyOn(notify, 'notifyMe'); + + vm.mr.ciStatus = 'failed'; + vm.mr.gitlabLogo = 'logo.png'; + }); + + it('should call notifyMe', () => { + vm.handleNotification(data); + + expect(notify.notifyMe).toHaveBeenCalledWith( + 'Pipeline running-label', + 'Pipeline running-label for "title"', + 'logo.png', + ); + }); + + it('should not call notifyMe if the status has not changed', () => { + vm.mr.ciStatus = data.ci_status; + + vm.handleNotification(data); + + expect(notify.notifyMe).not.toHaveBeenCalled(); + }); + }); + describe('resumePolling', () => { it('should call stopTimer on pollingInterval', () => { spyOn(vm.pollingInterval, 'resume'); diff --git a/spec/lib/gitlab/backup/repository_spec.rb b/spec/lib/gitlab/backup/repository_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..51c1e9d657b4ed91e20b2bca0caa5278d7915354 --- /dev/null +++ b/spec/lib/gitlab/backup/repository_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe Backup::Repository, lib: true do + let(:progress) { StringIO.new } + let!(:project) { create(:empty_project) } + + before do + allow(progress).to receive(:puts) + allow(progress).to receive(:print) + + allow_any_instance_of(String).to receive(:color) do |string, _color| + string + end + + allow_any_instance_of(described_class).to receive(:progress).and_return(progress) + end + + describe '#dump' do + describe 'repo failure' do + before do + allow_any_instance_of(Repository).to receive(:empty_repo?).and_raise(Rugged::OdbError) + allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) + end + + it 'does not raise error' do + expect { described_class.new.dump }.not_to raise_error + end + + it 'shows the appropriate error' do + described_class.new.dump + + expect(progress).to have_received(:puts).with("Ignoring repository error and continuing backing up project: #{project.full_path} - Rugged::OdbError") + end + end + + describe 'command failure' do + before do + allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false) + allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + end + + it 'shows the appropriate error' do + described_class.new.dump + + expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") + end + end + end + + describe '#restore' do + describe 'command failure' do + before do + allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + end + + it 'shows the appropriate error' do + described_class.new.restore + + expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") + end + end + end +end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index eb4f06b371c4346b5b857b9d9a74e57168496c5f..13e6953147bca8246bd4f9d8241c8946ca7b25ca 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -58,9 +58,12 @@ end end - context 'and user does have deployment permission' do + context 'and user has deployment permission' do before do - build.project.add_master(user) + build.project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) end it 'returns action' do diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb index b33389d959e30dd52e93cde9aad3cf2af515e15c..46dbdeae37cea2475caca685462c01da3eef5814 100644 --- a/spec/lib/gitlab/chat_commands/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb @@ -7,7 +7,12 @@ let(:regex_match) { described_class.match('deploy staging to production') } before do - project.add_master(user) + # Make it possible to trigger protected manual actions for developers. + # + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: 'master', project: project) end subject do diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 185bb9098dac01ccd01022e7c049f58dbdc72210..3f30b2c38f21a4b508002bbc081bfc1105c6aa27 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -224,7 +224,10 @@ context 'when user has ability to play action' do before do - build.project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) end it 'fabricates status that has action' do diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index f5d0f977768fc925e43d362c04a84eedefb9368a..0e15a5f3c6b428821e0d242c7e0dbc48f34945ee 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -2,6 +2,7 @@ describe Gitlab::Ci::Status::Build::Play do let(:user) { create(:user) } + let(:project) { build.project } let(:build) { create(:ci_build, :manual) } let(:status) { Gitlab::Ci::Status::Core.new(build, user) } @@ -15,8 +16,13 @@ describe '#has_action?' do context 'when user is allowed to update build' do - context 'when user can push to branch' do - before { build.project.add_master(user) } + context 'when user is allowed to trigger protected action' do + before do + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) + end it { is_expected.to have_action } end diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index 40ac5a3ed375f43ad95d3c7bd0853c507776750d..bbb3f9912a3e92401c0e86ba2af992fa7b194c1b 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -240,9 +240,50 @@ end context 'multiple results in content & regex' do - let(:data) { ' (98.39%) covered. (98.29%) covered' } + let(:data) do + <<~HEREDOC + (98.39%) covered + (98.29%) covered + HEREDOC + end + + let(:regex) { '\(\d+.\d+\%\) covered' } + + it 'returns the last matched coverage' do + is_expected.to eq("98.29") + end + end + + context 'when BUFFER_SIZE is smaller than stream.size' do + let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' } + let(:regex) { '\(\d+.\d+\%\) covered' } + + before do + stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5) + end + + it { is_expected.to eq("98.29") } + end + + context 'when regex is multi-byte char' do + let(:data) { '95.0 ゴッドファット\n' } + let(:regex) { '\d+\.\d+ ゴッドファット' } + + before do + stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5) + end + + it { is_expected.to eq('95.0') } + end + + context 'when BUFFER_SIZE is equal to stream.size' do + let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' } let(:regex) { '\(\d+.\d+\%\) covered' } + before do + stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', data.length) + end + it { is_expected.to eq("98.29") } end diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb index 24df04e985a18917ec2667668bd727cca55c6e0b..3c6ef7c7ccb29527f80f8c92a1cdc317e32cb115 100644 --- a/spec/lib/gitlab/etag_caching/middleware_spec.rb +++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb @@ -164,6 +164,25 @@ end end + context 'when GitLab instance is using a relative URL' do + before do + mock_app_response + end + + it 'uses full path as cache key' do + env = { + 'PATH_INFO' => enabled_path, + 'SCRIPT_NAME' => '/relative-gitlab' + } + + expect_any_instance_of(Gitlab::EtagCaching::Store) + .to receive(:get).with("/relative-gitlab#{enabled_path}") + .and_return(nil) + + middleware.call(env) + end + end + def mock_app_response allow(app).to receive(:call).and_return([app_status_code, {}, ['body']]) end diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb index 410df116a3a8dc442f6ee10936d754204078f125..5eafce9cb8f3d6be0dc46ee17b8982a136656d6f 100644 --- a/spec/lib/gitlab/etag_caching/router_spec.rb +++ b/spec/lib/gitlab/etag_caching/router_spec.rb @@ -2,93 +2,93 @@ describe Gitlab::EtagCaching::Router do it 'matches issue notes endpoint' do - env = build_env( + request = build_request( '/my-group/and-subgroup/here-comes-the-project/noteable/issue/1/notes' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'issue_notes' end it 'matches issue title endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/issues/123/rendered_title' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'issue_title' end it 'matches project pipelines endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/pipelines.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'project_pipelines' end it 'matches commit pipelines endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/commit/aa8260d253a53f73f6c26c734c72fdd600f6e6d4/pipelines.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'commit_pipelines' end it 'matches new merge request pipelines endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/merge_requests/new.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'new_merge_request_pipelines' end it 'matches merge request pipelines endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/merge_requests/234/pipelines.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'merge_request_pipelines' end it 'does not match blob with confusing name' do - env = build_env( + request = build_request( '/my-group/my-project/blob/master/pipelines.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_blank end it 'matches pipeline#show endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/pipelines/2.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'project_pipeline' end - def build_env(path) - { 'PATH_INFO' => path } + def build_request(path) + double(path_info: path) end end diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 822b98c5f6c9a40575d5d7f96b6c92e1df0f57af..b00e7a735712fb29396e7789e15e19d8bdd22651 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -25,6 +25,14 @@ expect(pipeline_schedule).not_to be_valid end + + context 'when active is false' do + it 'does not allow nullified ref' do + pipeline_schedule = build(:ci_pipeline_schedule, :inactive, ref: nil) + + expect(pipeline_schedule).not_to be_valid + end + end end describe '#set_next_run_at' do diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index bf75ab0f163d4819d304ad3a00f19720ba1c8132..7a967d77adaf5d7e8ba249e621638016c4ef1324 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -227,7 +227,10 @@ context 'when user is allowed to stop environment' do before do - project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: 'master', project: project) end context 'when action did not yet finish' do diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 80ca19acddae6a8255261680883e067dca6fb26e..84867e3d96b1fcc7a6f928d8efb8cdfa5c7e7548 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -49,6 +49,23 @@ expect(label.color).to eq('#abcdef') end + + it 'uses default color if color is missing' do + label = described_class.new(color: nil) + + expect(label.color).to be(Label::DEFAULT_COLOR) + end + end + + describe '#text_color' do + it 'uses default color if color is missing' do + expect(LabelsHelper).to receive(:text_color_for_bg).with(Label::DEFAULT_COLOR). + and_return(spy) + + label = described_class.new(color: nil) + + label.text_color + end end describe '#title' do diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 432beb7e693ba84c6978e29a2d7c6d0053cc6c9a..22f96f5dae2e8a655a905bb8fc4b0431895bd5a3 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -251,4 +251,17 @@ expect(milestone.to_reference(another_project)).to eq "sample-project%1" end end + + describe '#participants' do + let(:project) { build(:empty_project, name: 'sample-project') } + let(:milestone) { build(:milestone, iid: 1, project: project) } + + it 'returns participants without duplicates' do + user = create :user + create :issue, project: project, milestone: milestone, assignees: [user] + create :issue, project: project, milestone: milestone, assignees: [user] + + expect(milestone.participants).to eq [user] + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 2736541081eba2d0828b51ee697200ce07d3e104..45915dbaeeb8bf90fa51b2ae74aa7d1fba915d35 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2019,19 +2019,43 @@ def create_remote_branch(remote_name, branch_name, target) end describe '#is_ancestor?' do - context 'Gitaly is_ancestor feature enabled' do - let(:commit) { repository.commit } - let(:ancestor) { commit.parents.first } + let(:commit) { repository.commit } + let(:ancestor) { commit.parents.first } + context 'with Gitaly enabled' do + it 'it is an ancestor' do + expect(repository.is_ancestor?(ancestor.id, commit.id)).to eq(true) + end + + it 'it is not an ancestor' do + expect(repository.is_ancestor?(commit.id, ancestor.id)).to eq(false) + end + + it 'returns false on nil-values' do + expect(repository.is_ancestor?(nil, commit.id)).to eq(false) + expect(repository.is_ancestor?(ancestor.id, nil)).to eq(false) + expect(repository.is_ancestor?(nil, nil)).to eq(false) + end + end + + context 'with Gitaly disabled' do before do - allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(true) - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true) + allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(false) + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(false) end - it "asks Gitaly server if it's an ancestor" do - expect_any_instance_of(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).with(ancestor.id, commit.id) + it 'it is an ancestor' do + expect(repository.is_ancestor?(ancestor.id, commit.id)).to eq(true) + end + + it 'it is not an ancestor' do + expect(repository.is_ancestor?(commit.id, ancestor.id)).to eq(false) + end - repository.is_ancestor?(ancestor.id, commit.id) + it 'returns false on nil-values' do + expect(repository.is_ancestor?(nil, commit.id)).to eq(false) + expect(repository.is_ancestor?(ancestor.id, nil)).to eq(false) + expect(repository.is_ancestor?(nil, nil)).to eq(false) end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c829d045eb3c112e01735f4a6cd20177be26eac3..94e89af52cb6308e6674442eda7b32e9ad9ff3b3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1307,6 +1307,19 @@ it { is_expected.to eq([private_group]) } end + describe '#groups_through_project_authorizations' do + it 'returns all groups being ancestor of the authorized project' do + user = create(:user) + group = create(:group, :private) + subgroup = create(:group, :private, parent: group) + subsubgroup = create(:group, :private, parent: subgroup) + project = create(:empty_project, :private, namespace: subsubgroup) + project.add_guest(user) + + expect(user.groups_through_project_authorizations).to contain_exactly(group, subgroup, subsubgroup) + end + end + describe '#authorized_projects', truncate: true do context 'with a minimum access level' do it 'includes projects for which the user is an owner' do @@ -1949,6 +1962,34 @@ def add_user(access) end end + context '#invalidate_issue_cache_counts' do + let(:user) { build_stubbed(:user) } + + it 'invalidates cache for issue counter' do + cache_mock = double + + expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count']) + + allow(Rails).to receive(:cache).and_return(cache_mock) + + user.invalidate_issue_cache_counts + end + end + + context '#invalidate_merge_request_cache_counts' do + let(:user) { build_stubbed(:user) } + + it 'invalidates cache for Merge Request counter' do + cache_mock = double + + expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_merge_requests_count']) + + allow(Rails).to receive(:cache).and_return(cache_mock) + + user.invalidate_merge_request_cache_counts + end + end + describe '#forget_me!' do subject { create(:user, remember_created_at: Time.now) } diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb index b5eb84ae43b840f5d19444de8d794ce7a43fa2a3..6d5e1046e86003f2b3b0b05a0b094862c219fc33 100644 --- a/spec/serializers/build_entity_spec.rb +++ b/spec/serializers/build_entity_spec.rb @@ -3,6 +3,7 @@ describe BuildEntity do let(:user) { create(:user) } let(:build) { create(:ci_build) } + let(:project) { build.project } let(:request) { double('request') } before do @@ -52,7 +53,10 @@ context 'when user is allowed to trigger action' do before do - build.project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: 'master', project: project) end it 'contains path to play action' do diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb index d6f9fa42045b3c6cc5b2a2af01bde7bfc1a0a41f..ea211de1f82b8ee3ed5c21e423d532028f9c8bc5 100644 --- a/spec/services/ci/play_build_service_spec.rb +++ b/spec/services/ci/play_build_service_spec.rb @@ -13,8 +13,11 @@ context 'when project does not have repository yet' do let(:project) { create(:empty_project) } - it 'allows user with master role to play build' do - project.add_master(user) + it 'allows user to play build if protected branch rules are met' do + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) service.execute(build) @@ -45,7 +48,10 @@ let(:build) { create(:ci_build, :manual, pipeline: pipeline) } before do - project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) end it 'enqueues the build' do @@ -64,7 +70,10 @@ let(:build) { create(:ci_build, when: :manual, pipeline: pipeline) } before do - project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) end it 'duplicates the build' do diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index fc5de5d069a196cbf10d03622e35b04b6f29f816..1557cb3c9381dd96dcd3cb2e536b45aa69527e71 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -333,10 +333,11 @@ context 'when pipeline is promoted sequentially up to the end' do before do - # We are using create(:empty_project), and users has to be master in - # order to execute manual action when repository does not exist. + # Users need ability to merge into a branch in order to trigger + # protected manual actions. # - project.add_master(user) + create(:protected_branch, :developers_can_merge, + name: 'master', project: project) end it 'properly processes entire pipeline' do diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index d941d56c0d8f4756e0f6dd7aa2e3f04d448d2c26..3e860203063cecbe794ff83b86d5cbb26330eb04 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -6,9 +6,12 @@ let(:pipeline) { create(:ci_pipeline, project: project) } let(:service) { described_class.new(project, user) } - context 'when user has ability to modify pipeline' do + context 'when user has full ability to modify pipeline' do before do - project.add_master(user) + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: pipeline.ref, project: project) end context 'when there are already retried jobs present' do diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index 8fd5621475232f63f02ffcf8b814fdb9dedf622a..53c2675078912e842d065911f1937e8f943df210 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -72,7 +72,7 @@ def bulk_update(issuables, extra_params = {}) end context "when the new assignee ID is #{IssuableFinder::NONE}" do - it "unassigns the issues" do + it 'unassigns the issues' do expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) } .to change { merge_request.reload.assignee }.to(nil) end diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 5184053171186d549976db34d8ea90478a0e2e4f..52f2066d9c5dc645f6b37fa01cdebe817f5891d5 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -41,6 +41,12 @@ service.execute(issue) end + + it 'invalidates counter cache for assignees' do + expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts) + + service.execute(issue) + end end describe '#close_issue' do diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb index 93a8270fd16e477c97aab4842539e19eb76c032b..391ecad303a7e7722f7260c86cd218f76a9ea779 100644 --- a/spec/services/issues/reopen_service_spec.rb +++ b/spec/services/issues/reopen_service_spec.rb @@ -27,6 +27,13 @@ project.team << [user, :master] end + it 'invalidates counter cache for assignees' do + issue.assignees << user + expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts) + + described_class.new(project, user).execute(issue) + end + context 'when issue is not confidential' do it 'executes issue hooks' do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index d55a7657c0ea2acb160ae58aa1c448ee427ce660..154f30aac3b4ab2ed2878eebf885553e9cf9aa15 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -15,6 +15,8 @@ end describe '#execute' do + it_behaves_like 'cache counters invalidator' + context 'valid params' do let(:service) { described_class.new(project, user, {}) } diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a20b32eda5f1aa90992463b1255a33d2293e4af7 --- /dev/null +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe MergeRequests::PostMergeService, services: true do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, assignee: user) } + let(:project) { merge_request.project } + + before do + project.team << [user, :master] + end + + describe '#execute' do + it_behaves_like 'cache counters invalidator' + end +end diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index a99d4eac9bde5db6288496b3c8cc6597218029e0..b6d4db2f922e638e65b49a26966e7e0b5cd799cd 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -14,6 +14,8 @@ end describe '#execute' do + it_behaves_like 'cache counters invalidator' + context 'valid params' do let(:service) { described_class.new(project, user, {}) } diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb index d837184382f5c2568784d22bd1e0499042151bdd..0edcc50ed7b22f347aa7d1866399d21ea890b240 100644 --- a/spec/services/notes/slash_commands_service_spec.rb +++ b/spec/services/notes/slash_commands_service_spec.rb @@ -220,31 +220,4 @@ let(:note) { build(:note_on_commit, project: project) } end end - - context 'CE restriction for issue assignees' do - describe '/assign' do - let(:project) { create(:empty_project) } - let(:master) { create(:user).tap { |u| project.team << [u, :master] } } - let(:assignee) { create(:user) } - let(:master) { create(:user) } - let(:service) { described_class.new(project, master) } - let(:note) { create(:note_on_issue, note: note_text, project: project) } - - let(:note_text) do - %(/assign @#{assignee.username} @#{master.username}\n") - end - - before do - project.team << [master, :master] - project.team << [assignee, :master] - end - - it 'adds only one assignee from the list' do - _, command_params = service.extract_commands(note) - service.execute(command_params, note) - - expect(note.noteable.assignees.count).to eq(2) - end - end - end end diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index 40427fc2173d9f58d1b94acc803b6f51107977ba..553511328b751311452c602545b593f67a24c80c 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -401,7 +401,7 @@ it 'fetches assignee and populates assignee_id if content contains /assign' do _, updates = service.execute(content, issue) - expect(updates[:assignee_ids]).to match_array([developer.id, developer2.id]) + expect(updates[:assignee_ids]).to match_array([developer.id]) end end diff --git a/spec/support/issuable_shared_examples.rb b/spec/support/issuable_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..03011535351e0c1c56b11080eb20ed6afa613e78 --- /dev/null +++ b/spec/support/issuable_shared_examples.rb @@ -0,0 +1,7 @@ +shared_examples 'cache counters invalidator' do + it 'invalidates counter cache for assignees' do + expect_any_instance_of(User).to receive(:invalidate_merge_request_cache_counts) + + described_class.new(project, user, {}).execute(merge_request) + end +end diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c3b72e7d677e39c01c824794a5f1d85ec033d087 --- /dev/null +++ b/spec/uploaders/lfs_object_uploader_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe LfsObjectUploader do + let(:uploader) { described_class.new(build_stubbed(:empty_project)) } + + describe '#cache!' do + it 'caches the file in the cache directory' do + # One to get the work dir, the other to remove it + expect(uploader).to receive(:workfile_path).exactly(2).times.and_call_original + expect(FileUtils).to receive(:mv).with(anything, /^#{uploader.work_dir}/).and_call_original + expect(FileUtils).to receive(:mv).with(/^#{uploader.work_dir}/, /^#{uploader.cache_dir}/).and_call_original + + fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg') + uploader.cache!(fixture_file_upload(fixture)) + + expect(uploader.file.path).to start_with(uploader.cache_dir) + end + end + + describe '#move_to_cache' do + it 'is true' do + expect(uploader.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(uploader.move_to_store).to eq(true) + end + end +end