diff options
326 files changed, 4477 insertions, 3366 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 269fb5dfe2c..e030a0157c9 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.10.2 +5.10.3 @@ -405,8 +405,9 @@ gem 'gitaly-proto', '~> 0.59.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false # Feature toggles -gem 'flipper', '~> 0.10.2' -gem 'flipper-active_record', '~> 0.10.2' +gem 'flipper', '~> 0.11.0' +gem 'flipper-active_record', '~> 0.11.0' +gem 'flipper-active_support_cache_store', '~> 0.11.0' # Structured logging gem 'lograge', '~> 0.5' diff --git a/Gemfile.lock b/Gemfile.lock index efae71efdb7..f2546efa906 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -215,10 +215,13 @@ GEM path_expander (~> 1.0) ruby_parser (~> 3.0) sexp_processor (~> 4.0) - flipper (0.10.2) - flipper-active_record (0.10.2) + flipper (0.11.0) + flipper-active_record (0.11.0) activerecord (>= 3.2, < 6) - flipper (~> 0.10.2) + flipper (~> 0.11.0) + flipper-active_support_cache_store (0.11.0) + activesupport (>= 3.2, < 6) + flipper (~> 0.11.0) flowdock (0.7.1) httparty (~> 0.7) multi_json @@ -1021,8 +1024,9 @@ DEPENDENCIES faraday (~> 0.12) ffaker (~> 2.4) flay (~> 2.8.0) - flipper (~> 0.10.2) - flipper-active_record (~> 0.10.2) + flipper (~> 0.11.0) + flipper-active_record (~> 0.11.0) + flipper-active_support_cache_store (~> 0.11.0) fog-aliyun (~> 0.2.0) fog-aws (~> 1.4) fog-core (~> 1.44) diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index 5d060165f4b..f5f6b67f26e 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -2,8 +2,9 @@ /* global Pager */ import Cookies from 'js-cookie'; +import { localTimeAgo } from './lib/utils/datetime_utility'; -class Activities { +export default class Activities { constructor() { Pager.init(20, true, false, data => data, this.updateTooltips); @@ -15,7 +16,7 @@ class Activities { } updateTooltips() { - gl.utils.localTimeAgo($('.js-timeago', '.content_list')); + localTimeAgo($('.js-timeago', '.content_list')); } reloadActivities() { @@ -33,6 +34,3 @@ class Activities { $sender.closest('li').toggleClass('active'); } } - -window.gl = window.gl || {}; -window.gl.Activities = Activities; diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index b0b72c40f25..c1f7fa2aced 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,63 +1,59 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */ import { refreshCurrentPage } from './lib/utils/url_utility'; -window.Admin = (function() { - function Admin() { - var modal, showBlacklistType; - $('input#user_force_random_password').on('change', function(elem) { - var elems; - elems = $('#user_password, #user_password_confirmation'); - if ($(this).attr('checked')) { - return elems.val('').attr('disabled', true); - } else { - return elems.removeAttr('disabled'); - } - }); - $('body').on('click', '.js-toggle-colors-link', function(e) { - e.preventDefault(); - return $('.js-toggle-colors-container').toggle(); - }); - $('.log-tabs a').click(function(e) { - e.preventDefault(); - return $(this).tab('show'); - }); - $('.log-bottom').click(function(e) { - var visible_log; - e.preventDefault(); - visible_log = $(".file-content:visible"); - return visible_log.animate({ - scrollTop: visible_log.find('ol').height() - }, "fast"); - }); - modal = $('.change-owner-holder'); - $('.change-owner-link').bind("click", function(e) { - e.preventDefault(); - $(this).hide(); - return modal.show(); - }); - $('.change-owner-cancel-link').bind("click", function(e) { - e.preventDefault(); - modal.hide(); - return $('.change-owner-link').show(); - }); - $('li.project_member').bind('ajax:success', function() { - return refreshCurrentPage(); - }); - $('li.group_member').bind('ajax:success', function() { - return refreshCurrentPage(); - }); - showBlacklistType = function() { - if ($("input[name='blacklist_type']:checked").val() === 'file') { - $('.blacklist-file').show(); - return $('.blacklist-raw').hide(); - } else { - $('.blacklist-file').hide(); - return $('.blacklist-raw').show(); - } - }; - $("input[name='blacklist_type']").click(showBlacklistType); - showBlacklistType(); +function showBlacklistType() { + if ($('input[name="blacklist_type"]:checked').val() === 'file') { + $('.blacklist-file').show(); + $('.blacklist-raw').hide(); + } else { + $('.blacklist-file').hide(); + $('.blacklist-raw').show(); } +} - return Admin; -})(); +export default function adminInit() { + const modal = $('.change-owner-holder'); + + $('input#user_force_random_password').on('change', function randomPasswordClick() { + const $elems = $('#user_password, #user_password_confirmation'); + if ($(this).attr('checked')) { + $elems.val('').attr('disabled', true); + } else { + $elems.removeAttr('disabled'); + } + }); + + $('body').on('click', '.js-toggle-colors-link', (e) => { + e.preventDefault(); + $('.js-toggle-colors-container').toggle(); + }); + + $('.log-tabs a').on('click', function logTabsClick(e) { + e.preventDefault(); + $(this).tab('show'); + }); + + $('.log-bottom').on('click', (e) => { + e.preventDefault(); + const $visibleLog = $('.file-content:visible'); + $visibleLog.animate({ + scrollTop: $visibleLog.find('ol').height(), + }, 'fast'); + }); + + $('.change-owner-link').on('click', function changeOwnerLinkClick(e) { + e.preventDefault(); + $(this).hide(); + modal.show(); + }); + + $('.change-owner-cancel-link').on('click', (e) => { + e.preventDefault(); + modal.hide(); + $('.change-owner-link').show(); + }); + + $('li.project_member, li.group_member').on('ajax:success', refreshCurrentPage); + + $("input[name='blacklist_type']").on('click', showBlacklistType); + showBlacklistType(); +} diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js deleted file mode 100644 index 88756884d16..00000000000 --- a/app/assets/javascripts/aside.js +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */ - -window.Aside = (function() { - function Aside() { - $(document).off("click", "a.show-aside"); - $(document).on("click", 'a.show-aside', function(e) { - var btn, icon; - e.preventDefault(); - btn = $(e.currentTarget); - icon = btn.find('i'); - if (icon.hasClass('fa-angle-left')) { - btn.parent().find('section').hide(); - btn.parent().find('aside').fadeIn(); - return icon.removeClass('fa-angle-left').addClass('fa-angle-right'); - } else { - btn.parent().find('aside').hide(); - btn.parent().find('section').fadeIn(); - return icon.removeClass('fa-angle-right').addClass('fa-angle-left'); - } - }); - } - - return Aside; -})(); diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 9b952ea7b60..be58392135c 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -4,6 +4,7 @@ /* global Pager */ import { pluralize } from './lib/utils/text_utility'; +import { localTimeAgo } from './lib/utils/datetime_utility'; export default (function () { const CommitsList = {}; @@ -91,7 +92,7 @@ export default (function () { $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`); } - gl.utils.localTimeAgo($processedData.find('.js-timeago')); + localTimeAgo($processedData.find('.js-timeago')); return processedData; }; diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index 0ce467a3bd4..144caf1d278 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,4 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ +import { localTimeAgo } from './lib/utils/datetime_utility'; export default class Compare { constructor(opts) { @@ -81,7 +82,7 @@ export default class Compare { loading.hide(); $target.html(html); var className = '.' + $target[0].className.replace(' ', '.'); - gl.utils.localTimeAgo($('.js-timeago', className)); + localTimeAgo($('.js-timeago', className)); } }); } diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index cd20dde2951..74520675a7c 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -9,7 +9,7 @@ export default class ContextualSidebar { } initDomElements() { - this.$page = $('.page-with-sidebar'); + this.$page = $('.layout-page'); this.$sidebar = $('.nav-sidebar'); this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar); this.$overlay = $('.mobile-overlay'); diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index b41d464475f..2a05c6f001e 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -1,5 +1,6 @@ <script> import actionBtn from './action_btn.vue'; + import { getTimeago } from '../../lib/utils/datetime_utility'; export default { props: { @@ -21,7 +22,7 @@ }, computed: { timeagoDate() { - return gl.utils.getTimeago().format(this.deployKey.created_at); + return getTimeago().format(this.deployKey.created_at); }, editDeployKeyPath() { return `${this.endpoint}/${this.deployKey.id}/edit`; diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js index dc43e4b2cc7..1b8a9af9390 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js +++ b/app/assets/javascripts/diff_notes/models/discussion.js @@ -2,6 +2,7 @@ /* global NoteModel */ import Vue from 'vue'; +import { localTimeAgo } from '../../lib/utils/datetime_utility'; class DiscussionModel { constructor (discussionId) { @@ -71,7 +72,7 @@ class DiscussionModel { $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html); } - gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`)); + localTimeAgo($('.js-timeago', `${discussionSelector}`)); } else { $discussionHeadline.remove(); } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 299e43a4e90..522f5d12b30 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -15,8 +15,8 @@ import GroupLabelSubscription from './group_label_subscription'; import BuildArtifacts from './build_artifacts'; import CILintEditor from './ci_lint_editor'; import groupsSelect from './groups_select'; -/* global Search */ -/* global Admin */ +import Search from './search'; +import initAdmin from './admin'; import NamespaceSelect from './namespace_select'; import NewCommitForm from './new_commit_form'; import Project from './project'; @@ -24,7 +24,7 @@ import projectAvatar from './project_avatar'; /* global MergeRequest */ import Compare from './compare'; import initCompareAutocomplete from './compare_autocomplete'; -/* global ProjectFindFile */ +import ProjectFindFile from './project_find_file'; import ProjectNew from './project_new'; import projectImport from './project_import'; import Labels from './labels'; @@ -91,6 +91,8 @@ import DueDateSelectors from './due_date_select'; import Diff from './diff'; import ProjectLabelSubscription from './project_label_subscription'; import ProjectVariables from './project_variables'; +import SearchAutocomplete from './search_autocomplete'; +import Activities from './activities'; (function() { var Dispatcher; @@ -333,7 +335,7 @@ import ProjectVariables from './project_variables'; shortcut_handler = new ShortcutsIssuable(true); break; case 'dashboard:activity': - new gl.Activities(); + new Activities(); break; case 'projects:commit:show': new Diff(); @@ -354,7 +356,7 @@ import ProjectVariables from './project_variables'; $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); break; case 'projects:activity': - new gl.Activities(); + new Activities(); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:commits:show': @@ -372,7 +374,7 @@ import ProjectVariables from './project_variables'; if ($('#tree-slider').length) new TreeView(); if ($('.blob-viewer').length) new BlobViewer(); - if ($('.project-show-activity').length) new gl.Activities(); + if ($('.project-show-activity').length) new Activities(); $('#tree-slider').waitForImages(function() { ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); @@ -406,7 +408,7 @@ import ProjectVariables from './project_variables'; }); break; case 'groups:activity': - new gl.Activities(); + new Activities(); break; case 'groups:show': const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); @@ -583,7 +585,7 @@ import ProjectVariables from './project_variables'; // needed in rspec gl.u2fAuthenticate = u2fAuthenticate; case 'admin': - new Admin(); + initAdmin(); switch (path[1]) { case 'broadcast_messages': initBroadcastMessagesForm(); @@ -683,7 +685,7 @@ import ProjectVariables from './project_variables'; Dispatcher.prototype.initSearch = function() { // Only when search form is present if ($('.search').length) { - return new gl.SearchAutocomplete(); + return new SearchAutocomplete(); } }; diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index 09cb79c1afd..58ba5aff7cf 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -1,7 +1,7 @@ <script> import { s__ } from '../../locale'; import tooltip from '../../vue_shared/directives/tooltip'; -import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; +import modal from '../../vue_shared/components/modal.vue'; import eventHub from '../event_hub'; import { COMMON_STR } from '../constants'; import Icon from '../../vue_shared/components/icon.vue'; @@ -9,7 +9,7 @@ import Icon from '../../vue_shared/components/icon.vue'; export default { components: { Icon, - PopupDialog, + modal, }, directives: { tooltip, @@ -27,7 +27,7 @@ export default { }, data() { return { - dialogStatus: false, + modalStatus: false, }; }, computed: { @@ -43,10 +43,10 @@ export default { }, methods: { onLeaveGroup() { - this.dialogStatus = true; + this.modalStatus = true; }, leaveGroup(leaveConfirmed) { - this.dialogStatus = false; + this.modalStatus = false; if (leaveConfirmed) { eventHub.$emit('leaveGroup', this.group, this.parentGroup); } @@ -82,8 +82,8 @@ export default { class="fa fa-sign-out" aria-hidden="true"/> </a> - <popup-dialog - v-show="dialogStatus" + <modal + v-show="modalStatus" :primary-button-label="__('Leave')" kind="warning" :title="__('Are you sure?')" diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index ba2b6737988..bf77b93b643 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -21,7 +21,7 @@ export default class IssuableBulkUpdateSidebar { } initDomElements() { - this.$page = $('.page-with-sidebar'); + this.$page = $('.layout-page'); this.$sidebar = $('.right-sidebar'); this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar'); this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide'); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 91b5ef1c10a..411c820cc43 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -48,7 +48,7 @@ export default class Issue { }) .fail(() => new Flash(issueFailMessage)) .done((data) => { - const isClosedBadge = $('div.status-box-closed'); + const isClosedBadge = $('div.status-box-issue-closed'); const isOpenBadge = $('div.status-box-open'); const projectIssuesCounter = $('.issue_counter'); diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index fd1a50dd533..25ebe5314e0 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -9,7 +9,7 @@ import titleComponent from './title.vue'; import descriptionComponent from './description.vue'; import editedComponent from './edited.vue'; import formComponent from './form.vue'; -import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor'; +import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; export default { props: { @@ -152,7 +152,7 @@ export default { }, mixins: [ - RecaptchaDialogImplementor, + recaptchaModalImplementor, ], methods: { @@ -197,7 +197,7 @@ export default { }); }, - closeRecaptchaDialog() { + closeRecaptchaModal() { this.store.setFormState({ updateLoading: false, }); @@ -273,10 +273,10 @@ export default { :enable-autocomplete="enableAutocomplete" /> - <recaptcha-dialog + <recaptcha-modal v-show="showRecaptcha" :html="recaptchaHTML" - @close="closeRecaptchaDialog" + @close="closeRecaptchaModal" /> </div> <div v-else> diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index feb73481422..c3f2bf130bb 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,12 +1,12 @@ <script> import animateMixin from '../mixins/animate'; import TaskList from '../../task_list'; - import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor'; + import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; export default { mixins: [ animateMixin, - RecaptchaDialogImplementor, + recaptchaModalImplementor, ], props: { @@ -126,7 +126,7 @@ > </textarea> - <recaptcha-dialog + <recaptcha-modal v-show="showRecaptcha" :html="recaptchaHTML" @close="closeRecaptcha" diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 52fe4ecd08b..4e577546551 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -53,7 +53,7 @@ <textarea id="issue-description" class="note-textarea js-gfm-input js-autosize markdown-area" - data-supports-quick-actionss="false" + data-supports-quick-actions="false" aria-label="Description" v-model="formState.description" ref="textarea" diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index 06b0e02a870..198a7823381 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -3,6 +3,7 @@ import { visitUrl } from './lib/utils/url_utility'; import bp from './breakpoints'; import { bytesToKiB } from './lib/utils/number_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils'; +import { timeFor } from './lib/utils/datetime_utility'; export default class Job { constructor(options) { @@ -261,7 +262,7 @@ export default class Job { if ($date.length) { const date = $date.text(); return $date.text( - gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '), + timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3'))), ); } } diff --git a/app/assets/javascripts/lib/utils/cache.js b/app/assets/javascripts/lib/utils/cache.js index 3141f1eeafc..596bd1e388a 100644 --- a/app/assets/javascripts/lib/utils/cache.js +++ b/app/assets/javascripts/lib/utils/cache.js @@ -1,4 +1,4 @@ -class Cache { +export default class Cache { constructor() { this.internalStorage = { }; } @@ -15,5 +15,3 @@ class Cache { delete this.internalStorage[key]; } } - -export default Cache; diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js index 7a72509d234..9a61003ef30 100644 --- a/app/assets/javascripts/lib/utils/constants.js +++ b/app/assets/javascripts/lib/utils/constants.js @@ -1,3 +1,2 @@ -/* eslint-disable import/prefer-default-export */ export const BYTES_IN_KIB = 1024; export const HIDDEN_CLASS = 'hidden'; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index d0578b230b1..198b5164c92 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,9 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */ - import timeago from 'timeago.js'; import dateFormat from 'vendor/date.format'; import { pluralize } from './text_utility'; - import { lang, s__, @@ -12,121 +9,125 @@ import { window.timeago = timeago; window.dateFormat = dateFormat; -(function() { - (function(w) { - var base; - var timeagoInstance; +/** + * Given a date object returns the day of the week in English + * @param {date} date + * @returns {String} + */ +export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()]; - if (w.gl == null) { - w.gl = {}; - } - if ((base = w.gl).utils == null) { - base.utils = {}; - } - w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; +/** + * @example + * dateFormat('2017-12-05','mmm d, yyyy h:MMtt Z' ) -> "Dec 5, 2017 12:00am GMT+0000" + * @param {date} datetime + * @returns {String} + */ +export const formatDate = datetime => dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); - w.gl.utils.formatDate = function(datetime) { - return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); +let timeagoInstance; +/** + * Sets a timeago Instance + */ +export function getTimeago() { + if (!timeagoInstance) { + const localeRemaining = function getLocaleRemaining(number, index) { + return [ + [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], + [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], + [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], + [s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')], + [s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')], + [s__('Timeago|a day ago'), s__('Timeago|1 day remaining')], + [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], + [s__('Timeago|a week ago'), s__('Timeago|1 week remaining')], + [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], + [s__('Timeago|a month ago'), s__('Timeago|1 month remaining')], + [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], + [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')], + [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], + ][index]; }; - - w.gl.utils.getDayName = function(date) { - return this.days[date.getDay()]; + const locale = function getLocale(number, index) { + return [ + [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], + [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], + [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], + [s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')], + [s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')], + [s__('Timeago|a day ago'), s__('Timeago|in 1 day')], + [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], + [s__('Timeago|a week ago'), s__('Timeago|in 1 week')], + [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], + [s__('Timeago|a month ago'), s__('Timeago|in 1 month')], + [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], + [s__('Timeago|a year ago'), s__('Timeago|in 1 year')], + [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], + ][index]; }; - w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) { - $timeagoEls.each((i, el) => { - if (setTimeago) { - // Recreate with custom template - $(el).tooltip({ - template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' - }); - } + timeago.register(lang, locale); + timeago.register(`${lang}-remaining`, localeRemaining); + timeagoInstance = timeago(); + } - el.classList.add('js-timeago-render'); - }); + return timeagoInstance; +} - gl.utils.renderTimeago($timeagoEls); - }; +/** + * For the given element, renders a timeago instance. + * @param {jQuery} $els + */ +export const renderTimeago = ($els) => { + const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); - w.gl.utils.getTimeago = function() { - var locale; - - if (!timeagoInstance) { - const localeRemaining = function(number, index) { - return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], - [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], - [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], - [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], - [s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')], - [s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')], - [s__('Timeago|a day ago'), s__('Timeago|1 day remaining')], - [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], - [s__('Timeago|a week ago'), s__('Timeago|1 week remaining')], - [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], - [s__('Timeago|a month ago'), s__('Timeago|1 month remaining')], - [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], - [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')], - [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')] - ][index]; - }; - locale = function(number, index) { - return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], - [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], - [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], - [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], - [s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')], - [s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')], - [s__('Timeago|a day ago'), s__('Timeago|in 1 day')], - [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], - [s__('Timeago|a week ago'), s__('Timeago|in 1 week')], - [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], - [s__('Timeago|a month ago'), s__('Timeago|in 1 month')], - [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], - [s__('Timeago|a year ago'), s__('Timeago|in 1 year')], - [s__('Timeago|%s years ago'), s__('Timeago|in %s years')] - ][index]; - }; - - timeago.register(lang, locale); - timeago.register(`${lang}-remaining`, localeRemaining); - timeagoInstance = timeago(); - } - - return timeagoInstance; - }; + // timeago.js sets timeouts internally for each timeago value to be updated in real time + getTimeago().render(timeagoEls, lang); +}; - w.gl.utils.timeFor = function(time, suffix, expiredLabel) { - var timefor; - if (!time) { - return ''; - } - if (new Date(time) < new Date()) { - expiredLabel || (expiredLabel = s__('Timeago|Past due')); - timefor = expiredLabel; - } else { - timefor = gl.utils.getTimeago().format(time, `${lang}-remaining`).trim(); - } - return timefor; - }; +/** + * For the given elements, sets a tooltip with a formatted date. + * @param {jQuery} + * @param {Boolean} setTimeago + */ +export const localTimeAgo = ($timeagoEls, setTimeago = true) => { + $timeagoEls.each((i, el) => { + if (setTimeago) { + // Recreate with custom template + $(el).tooltip({ + template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', + }); + } - w.gl.utils.renderTimeago = function($els) { - const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); + el.classList.add('js-timeago-render'); + }); - // timeago.js sets timeouts internally for each timeago value to be updated in real time - gl.utils.getTimeago().render(timeagoEls, lang); - }; + renderTimeago($timeagoEls); +}; - w.gl.utils.getDayDifference = function(a, b) { - var millisecondsPerDay = 1000 * 60 * 60 * 24; - var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); - var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); +/** + * Returns remaining or passed time over the given time. + * @param {*} time + * @param {*} expiredLabel + */ +export const timeFor = (time, expiredLabel) => { + if (!time) { + return ''; + } + if (new Date(time) < new Date()) { + return expiredLabel || s__('Timeago|Past due'); + } + return getTimeago().format(time, `${lang}-remaining`).trim(); +}; - return Math.floor((date2 - date1) / millisecondsPerDay); - }; - })(window); -}).call(window); +export const getDayDifference = (a, b) => { + const millisecondsPerDay = 1000 * 60 * 60 * 24; + const date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); + const date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); + + return Math.floor((date2 - date1) / millisecondsPerDay); +}; /** * Port of ruby helper time_interval_in_words. @@ -162,3 +163,10 @@ export function dateInWords(date, abbreviated = false) { return `${monthName} ${date.getDate()}, ${year}`; } + +window.gl = window.gl || {}; +window.gl.utils = { + ...(window.gl.utils || {}), + getTimeago, + localTimeAgo, +}; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9e4047b6840..96284c4c168 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ /* global ConfirmDangerModal */ -/* global Aside */ import jQuery from 'jquery'; import _ from 'underscore'; @@ -28,7 +27,7 @@ import './commit/image_file'; // lib/utils import { handleLocationHash } from './lib/utils/common_utils'; -import './lib/utils/datetime_utility'; +import { localTimeAgo, renderTimeago } from './lib/utils/datetime_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; // behaviors @@ -37,7 +36,6 @@ import './behaviors/'; // everything else import './activities'; import './admin'; -import './aside'; import loadAwardsHandler from './awards_handler'; import bp from './breakpoints'; import './confirm_danger_modal'; @@ -60,15 +58,10 @@ import './notifications_dropdown'; import './notifications_form'; import './pager'; import './preview_markdown'; -import './project_find_file'; import './project_import'; import './projects_dropdown'; -import './projects_list'; -import './syntax_highlight'; import './render_gfm'; import './right_sidebar'; -import './search'; -import './search_autocomplete'; import initBreadcrumbs from './breadcrumb'; import './dispatcher'; @@ -125,7 +118,7 @@ $(function () { }); if (bootstrapBreakpoint === 'xs') { - const $rightSidebar = $('aside.right-sidebar, .page-with-sidebar'); + const $rightSidebar = $('aside.right-sidebar, .layout-page'); $rightSidebar .removeClass('right-sidebar-expanded') @@ -185,13 +178,13 @@ $(function () { trigger: 'focus', // set the viewport to the main content, excluding the navigation bar, so // the navigation can't overlap the popover - viewport: '.page-with-sidebar' + viewport: '.layout-page' }); $('.trigger-submit').on('change', function () { return $(this).parents('form').submit(); // Form submitter }); - gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); + localTimeAgo($('abbr.timeago, .js-timeago'), true); // Disable form buttons while a form is submitting $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { var buttons; @@ -278,9 +271,8 @@ $(function () { return fitSidebarForSize(); }); loadAwardsHandler(); - new Aside(); - gl.utils.renderTimeago(); + renderTimeago(); $(document).trigger('init.scrolling-tabs'); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 17591829b76..94561d6b7c3 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -10,6 +10,7 @@ import './mixins/line_conflict_actions'; import './components/diff_file_editor'; import './components/inline_conflict_lines'; import './components/parallel_conflict_lines'; +import syntaxHighlight from '../syntax_highlight'; $(() => { const INTERACTIVE_RESOLVE_MODE = 'interactive'; @@ -53,7 +54,7 @@ $(() => { mergeConflictsStore.setLoadingState(false); this.$nextTick(() => { - $('.js-syntax-highlight').syntaxHighlight(); + syntaxHighlight($('.js-syntax-highlight')); }); }); }, diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 589e65647ac..de84e28f915 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -14,6 +14,8 @@ import { import { getLocationHash } from './lib/utils/url_utility'; import initDiscussionTab from './image_diff/init_discussion_tab'; import Diff from './diff'; +import { localTimeAgo } from './lib/utils/datetime_utility'; +import syntaxHighlight from './syntax_highlight'; /* eslint-disable max-len */ // MergeRequestTabs @@ -247,7 +249,7 @@ import Diff from './diff'; url: `${source}.json`, success: (data) => { document.querySelector('div#commits').innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); + localTimeAgo($('.js-timeago', 'div#commits')); this.commitsLoaded = true; this.scrollToElement('#commits'); }, @@ -294,8 +296,8 @@ import Diff from './diff'; gl.diffNotesCompileComponents(); } - gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); - $('#diffs .js-syntax-highlight').syntaxHighlight(); + localTimeAgo($('.js-timeago', 'div#diffs')); + syntaxHighlight($('#diffs .js-syntax-highlight')); if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { this.expandViewContainer(); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 74e5a4f1cea..2e5e818d61d 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -2,6 +2,7 @@ /* global Issuable */ /* global ListMilestone */ import _ from 'underscore'; +import { timeFor } from './lib/utils/datetime_utility'; (function() { this.MilestoneSelect = (function() { @@ -216,7 +217,7 @@ import _ from 'underscore'; $value.css('display', ''); if (data.milestone != null) { data.milestone.full_path = _this.currentProject.full_path; - data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date); + data.milestone.remaining = timeFor(data.milestone.due_date); data.milestone.name = data.milestone.title; $value.html(milestoneLinkTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 2a570ac705e..042fe44e1c6 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -25,6 +25,7 @@ import Autosave from './autosave'; import TaskList from './task_list'; import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; import imageDiffHelper from './image_diff/helpers/index'; +import { localTimeAgo } from './lib/utils/datetime_utility'; window.autosize = Autosize; @@ -311,7 +312,7 @@ export default class Notes { setupNewNote($note) { // Update datetime format on the recent note - gl.utils.localTimeAgo($note.find('.js-timeago'), false); + localTimeAgo($note.find('.js-timeago'), false); this.collapseLongCommitList(); this.taskList.init(); @@ -463,7 +464,7 @@ export default class Notes { this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); } - gl.utils.localTimeAgo($('.js-timeago'), false); + localTimeAgo($('.js-timeago'), false); Notes.checkMergeRequestStatus(); return this.updateNotesCount(1); } diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index 0eaac8dd64f..78322f30685 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -1,36 +1,41 @@ <script> -export default { - props: { - helpPagePath: { - type: String, - required: true, + export default { + props: { + helpPagePath: { + type: String, + required: true, + }, + emptyStateSvgPath: { + type: String, + required: true, + }, }, - emptyStateSvgPath: { - type: String, - required: true, - }, - }, -}; + }; </script> - <template> <div class="row empty-state js-empty-state"> <div class="col-xs-12"> - <div class="svg-content"> - <img :src="emptyStateSvgPath"/> + <div class="svg-content svg-250"> + <img :src="emptyStateSvgPath" /> </div> </div> - <div class="col-xs-12 text-center"> + <div class="col-xs-12"> <div class="text-content"> - <h4>Build with confidence</h4> + <h4 class="text-center"> + {{ s__("Pipelines|Build with confidence") }} + </h4> <p> - Continous Integration can help catch bugs by running your tests automatically, - while Continuous Deployment can help you deliver code to your product environment. + {{ s__("Pipelines|Continous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment.") }} </p> - <a :href="helpPagePath" class="btn btn-info"> - Get started with Pipelines - </a> + <div class="text-center"> + <a + :href="helpPagePath" + class="btn btn-info" + > + {{ s__("Pipelines|Get started with Pipelines") }} + </a> + </div> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pipelines/pipelines_bundle.js index 923d9bfb248..3e4b6eeb5bf 100644 --- a/app/assets/javascripts/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/pipelines/pipelines_bundle.js @@ -1,6 +1,9 @@ import Vue from 'vue'; import PipelinesStore from './stores/pipelines_store'; import pipelinesComponent from './components/pipelines.vue'; +import Translate from '../vue_shared/translate'; + +Vue.use(Translate); document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#pipelines-list-vue', diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 6348a2e331d..78be6b6e884 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,5 +1,5 @@ <script> - import popupDialog from '../../../vue_shared/components/popup_dialog.vue'; + import modal from '../../../vue_shared/components/modal.vue'; import { __, s__, sprintf } from '../../../locale'; import csrf from '../../../lib/utils/csrf'; @@ -26,7 +26,7 @@ }; }, components: { - popupDialog, + modal, }, computed: { csrfToken() { @@ -89,7 +89,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), <template> <div> - <popup-dialog + <modal v-if="isOpen" :title="s__('Profiles|Delete your account?')" :text="text" @@ -134,7 +134,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), </form> </template> - </popup-dialog> + </modal> <button type="button" diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 19682b20a4a..0da32b4a3cc 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -2,169 +2,163 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; -(function() { - this.ProjectFindFile = (function() { - var highlighter; - - function ProjectFindFile(element1, options) { - this.element = element1; - this.options = options; - this.goToBlob = this.goToBlob.bind(this); - this.goToTree = this.goToTree.bind(this); - this.selectRowDown = this.selectRowDown.bind(this); - this.selectRowUp = this.selectRowUp.bind(this); - this.filePaths = {}; - this.inputElement = this.element.find(".file-finder-input"); - // init event - this.initEvent(); - // focus text input box - this.inputElement.focus(); - // load file list - this.load(this.options.url); +// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) +const highlighter = function(element, text, matches) { + var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched; + lastIndex = 0; + highlightText = ""; + matchedChars = []; + for (j = 0, len = matches.length; j < len; j += 1) { + matchIndex = matches[j]; + unmatched = text.substring(lastIndex, matchIndex); + if (unmatched) { + if (matchedChars.length) { + element.append(matchedChars.join("").bold()); + } + matchedChars = []; + element.append(document.createTextNode(unmatched)); } - - ProjectFindFile.prototype.initEvent = function() { - this.inputElement.off("keyup"); - this.inputElement.on("keyup", (function(_this) { - return function(event) { - var oldValue, ref, target, value; - target = $(event.target); - value = target.val(); - oldValue = (ref = target.data("oldValue")) != null ? ref : ""; - if (value !== oldValue) { - target.data("oldValue", value); - _this.findFile(); - return _this.element.find("tr.tree-item").eq(0).addClass("selected").focus(); - } - }; - })(this)); - }; - - ProjectFindFile.prototype.findFile = function() { - var result, searchText; - searchText = this.inputElement.val(); - result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; - return this.renderList(result, searchText); - // find file - }; + matchedChars.push(text[matchIndex]); + lastIndex = matchIndex + 1; + } + if (matchedChars.length) { + element.append(matchedChars.join("").bold()); + } + return element.append(document.createTextNode(text.substring(lastIndex))); +}; + +export default class ProjectFindFile { + constructor(element1, options) { + this.element = element1; + this.options = options; + this.goToBlob = this.goToBlob.bind(this); + this.goToTree = this.goToTree.bind(this); + this.selectRowDown = this.selectRowDown.bind(this); + this.selectRowUp = this.selectRowUp.bind(this); + this.filePaths = {}; + this.inputElement = this.element.find(".file-finder-input"); + // init event + this.initEvent(); + // focus text input box + this.inputElement.focus(); + // load file list + this.load(this.options.url); + } + + initEvent() { + this.inputElement.off("keyup"); + this.inputElement.on("keyup", (function(_this) { + return function(event) { + var oldValue, ref, target, value; + target = $(event.target); + value = target.val(); + oldValue = (ref = target.data("oldValue")) != null ? ref : ""; + if (value !== oldValue) { + target.data("oldValue", value); + _this.findFile(); + return _this.element.find("tr.tree-item").eq(0).addClass("selected").focus(); + } + }; + })(this)); + } + + findFile() { + var result, searchText; + searchText = this.inputElement.val(); + result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; + return this.renderList(result, searchText); + // find file + } // files pathes load - ProjectFindFile.prototype.load = function(url) { - return $.ajax({ - url: url, - method: "get", - dataType: "json", - success: (function(_this) { - return function(data) { - _this.element.find(".loading").hide(); - _this.filePaths = data; - _this.findFile(); - return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus(); - }; - })(this) - }); - }; + load(url) { + return $.ajax({ + url: url, + method: "get", + dataType: "json", + success: (function(_this) { + return function(data) { + _this.element.find(".loading").hide(); + _this.filePaths = data; + _this.findFile(); + return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus(); + }; + })(this) + }); + } // render result - ProjectFindFile.prototype.renderList = function(filePaths, searchText) { - var blobItemUrl, filePath, html, i, j, len, matches, results; - this.element.find(".tree-table > tbody").empty(); - results = []; - for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) { - filePath = filePaths[i]; - if (i === 20) { - break; - } - if (searchText) { - matches = fuzzaldrinPlus.match(filePath, searchText); - } - blobItemUrl = this.options.blobUrlTemplate + "/" + filePath; - html = this.makeHtml(filePath, matches, blobItemUrl); - results.push(this.element.find(".tree-table > tbody").append(html)); - } - return results; - }; - - // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) - highlighter = function(element, text, matches) { - var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched; - lastIndex = 0; - highlightText = ""; - matchedChars = []; - for (j = 0, len = matches.length; j < len; j += 1) { - matchIndex = matches[j]; - unmatched = text.substring(lastIndex, matchIndex); - if (unmatched) { - if (matchedChars.length) { - element.append(matchedChars.join("").bold()); - } - matchedChars = []; - element.append(document.createTextNode(unmatched)); - } - matchedChars.push(text[matchIndex]); - lastIndex = matchIndex + 1; - } - if (matchedChars.length) { - element.append(matchedChars.join("").bold()); + renderList(filePaths, searchText) { + var blobItemUrl, filePath, html, i, j, len, matches, results; + this.element.find(".tree-table > tbody").empty(); + results = []; + for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) { + filePath = filePaths[i]; + if (i === 20) { + break; } - return element.append(document.createTextNode(text.substring(lastIndex))); - }; - - // make tbody row html - ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) { - var $tr; - $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>"); - if (matches) { - $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl)); - } else { - $tr.find("a").attr("href", blobItemUrl); - $tr.find(".str-truncated").text(filePath); + if (searchText) { + matches = fuzzaldrinPlus.match(filePath, searchText); } - return $tr; - }; - - ProjectFindFile.prototype.selectRow = function(type) { - var next, rows, selectedRow; - rows = this.element.find(".files-slider tr.tree-item"); - selectedRow = this.element.find(".files-slider tr.tree-item.selected"); - if (rows && rows.length > 0) { - if (selectedRow && selectedRow.length > 0) { - if (type === "UP") { - next = selectedRow.prev(); - } else if (type === "DOWN") { - next = selectedRow.next(); - } - if (next.length > 0) { - selectedRow.removeClass("selected"); - selectedRow = next; - } - } else { - selectedRow = rows.eq(0); + blobItemUrl = this.options.blobUrlTemplate + "/" + filePath; + html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl); + results.push(this.element.find(".tree-table > tbody").append(html)); + } + return results; + } + + // make tbody row html + static makeHtml(filePath, matches, blobItemUrl) { + var $tr; + $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>"); + if (matches) { + $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl)); + } else { + $tr.find("a").attr("href", blobItemUrl); + $tr.find(".str-truncated").text(filePath); + } + return $tr; + } + + selectRow(type) { + var next, rows, selectedRow; + rows = this.element.find(".files-slider tr.tree-item"); + selectedRow = this.element.find(".files-slider tr.tree-item.selected"); + if (rows && rows.length > 0) { + if (selectedRow && selectedRow.length > 0) { + if (type === "UP") { + next = selectedRow.prev(); + } else if (type === "DOWN") { + next = selectedRow.next(); + } + if (next.length > 0) { + selectedRow.removeClass("selected"); + selectedRow = next; } - return selectedRow.addClass("selected").focus(); + } else { + selectedRow = rows.eq(0); } - }; - - ProjectFindFile.prototype.selectRowUp = function() { - return this.selectRow("UP"); - }; + return selectedRow.addClass("selected").focus(); + } + } - ProjectFindFile.prototype.selectRowDown = function() { - return this.selectRow("DOWN"); - }; + selectRowUp() { + return this.selectRow("UP"); + } - ProjectFindFile.prototype.goToTree = function() { - return location.href = this.options.treeUrl; - }; + selectRowDown() { + return this.selectRow("DOWN"); + } - ProjectFindFile.prototype.goToBlob = function() { - var $link = this.element.find(".tree-item.selected .tree-item-file-name a"); + goToTree() { + return location.href = this.options.treeUrl; + } - if ($link.length) { - $link.get(0).click(); - } - }; + goToBlob() { + var $link = this.element.find(".tree-item.selected .tree-item-file-name a"); - return ProjectFindFile; - })(); -}).call(window); + if ($link.length) { + $link.get(0).click(); + } + } +} diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index c91a0d9ba41..5482c55f8bb 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -1,12 +1,12 @@ import renderMath from './render_math'; import renderMermaid from './render_mermaid'; - +import syntaxHighlight from './syntax_highlight'; // Render Gitlab flavoured Markdown // // Delegates to syntax highlight and render math & mermaid diagrams. // $.fn.renderGFM = function renderGFM() { - this.find('.js-syntax-highlight').syntaxHighlight(); + syntaxHighlight(this.find('.js-syntax-highlight')); renderMath(this.find('.js-render-math')); renderMermaid(this.find('.js-render-mermaid')); return this; diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue index ac1f613bb71..c191af7dec3 100644 --- a/app/assets/javascripts/repo/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue @@ -1,7 +1,7 @@ <script> import { mapActions } from 'vuex'; import { __ } from '../../../locale'; - import popupDialog from '../../../vue_shared/components/popup_dialog.vue'; + import modal from '../../../vue_shared/components/modal.vue'; export default { props: { @@ -20,7 +20,7 @@ }; }, components: { - popupDialog, + modal, }, methods: { ...mapActions([ @@ -68,7 +68,7 @@ </script> <template> - <popup-dialog + <modal :title="modalTitle" :primary-button-label="buttonLabel" kind="success" @@ -94,5 +94,5 @@ </div> </fieldset> </form> - </popup-dialog> + </modal> </template> diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index d3344d0c8dc..4e0178072cb 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -2,12 +2,12 @@ import { mapGetters, mapState, mapActions } from 'vuex'; import tooltip from '../../vue_shared/directives/tooltip'; import icon from '../../vue_shared/components/icon.vue'; -import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; +import modal from '../../vue_shared/components/modal.vue'; import commitFilesList from './commit_sidebar/list.vue'; export default { components: { - PopupDialog, + modal, icon, commitFilesList, }, @@ -16,7 +16,7 @@ export default { }, data() { return { - showNewBranchDialog: false, + showNewBranchModal: false, submitCommitsLoading: false, startNewMR: false, commitMessage: '', @@ -58,7 +58,7 @@ export default { start_branch: createNewBranch ? this.currentBranch : undefined, }; - this.showNewBranchDialog = false; + this.showNewBranchModal = false; this.submitCommitsLoading = true; this.commitChanges({ payload, newMr: this.startNewMR }) @@ -76,7 +76,7 @@ export default { this.checkCommitStatus() .then((branchChanged) => { if (branchChanged) { - this.showNewBranchDialog = true; + this.showNewBranchModal = true; } else { this.makeCommit(); } @@ -99,13 +99,13 @@ export default { 'is-collapsed': collapsed, }" > - <popup-dialog - v-if="showNewBranchDialog" + <modal + v-if="showNewBranchModal" :primary-button-label="__('Create new branch')" kind="primary" :title="__('Branch has changed')" :text="__('This branch has changed since you started editing. Would you like to create a new branch?')" - @toggle="showNewBranchDialog = false" + @toggle="showNewBranchModal = false" @submit="makeCommit(true)" /> <button diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue index 6c1bb4b8566..37bd9003e96 100644 --- a/app/assets/javascripts/repo/components/repo_edit_button.vue +++ b/app/assets/javascripts/repo/components/repo_edit_button.vue @@ -1,10 +1,10 @@ <script> import { mapGetters, mapActions, mapState } from 'vuex'; -import popupDialog from '../../vue_shared/components/popup_dialog.vue'; +import modal from '../../vue_shared/components/modal.vue'; export default { components: { - popupDialog, + modal, }, computed: { ...mapState([ @@ -43,7 +43,7 @@ export default { {{buttonLabel}} </span> </button> - <popup-dialog + <modal v-if="discardPopupOpen" class="text-left" :primary-button-label="__('Discard changes')" diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index 6ce9267f598..425c55fafb5 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -1,6 +1,7 @@ <script> /* global LineHighlighter */ import { mapGetters } from 'vuex'; +import syntaxHighlight from '../../syntax_highlight'; export default { computed: { @@ -13,7 +14,7 @@ export default { }, methods: { highlightFile() { - $(this.$el).find('.file-content').syntaxHighlight(); + syntaxHighlight($(this.$el).find('.file-content')); }, }, mounted() { diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index fa7f6825d7e..ec85b8b6529 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -42,11 +42,11 @@ import Cookies from 'js-cookie'; if ($thisIcon.hasClass('fa-angle-double-right')) { $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); - $('.page-with-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); } else { $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); - $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); if (gl.lazyLoader) gl.lazyLoader.loadCheck(); } @@ -173,7 +173,7 @@ import Cookies from 'js-cookie'; Sidebar.prototype.setCollapseAfterUpdate = function($block) { $block.addClass('collapse-after-update'); - return $('.page-with-sidebar').addClass('with-overlay'); + return $('.layout-page').addClass('with-overlay'); }; Sidebar.prototype.onSidebarDropdownHidden = function(e) { @@ -187,7 +187,7 @@ import Cookies from 'js-cookie'; Sidebar.prototype.sidebarDropdownHidden = function($block) { if ($block.hasClass('collapse-after-update')) { $block.removeClass('collapse-after-update'); - $('.page-with-sidebar').removeClass('with-overlay'); + $('.layout-page').removeClass('with-overlay'); return this.toggleSidebar('hide'); } }; diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 07fee53d814..363322af47a 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,118 +1,113 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */ import Flash from './flash'; import Api from './api'; -(function() { - this.Search = (function() { - function Search() { - var $groupDropdown, $projectDropdown; - $groupDropdown = $('.js-search-group-dropdown'); - $projectDropdown = $('.js-search-project-dropdown'); - this.groupId = $groupDropdown.data('group-id'); - this.eventListeners(); - $groupDropdown.glDropdown({ - selectable: true, - filterable: true, - fieldName: 'group_id', - search: { - fields: ['full_name'] - }, - data: function(term, callback) { - return Api.groups(term, {}, function(data) { +export default class Search { + constructor() { + const $groupDropdown = $('.js-search-group-dropdown'); + const $projectDropdown = $('.js-search-project-dropdown'); + + this.searchInput = '.js-search-input'; + this.searchClear = '.js-search-clear'; + + this.groupId = $groupDropdown.data('group-id'); + this.eventListeners(); + + $groupDropdown.glDropdown({ + selectable: true, + filterable: true, + fieldName: 'group_id', + search: { + fields: ['full_name'], + }, + data(term, callback) { + return Api.groups(term, {}, (data) => { + data.unshift({ + full_name: 'Any', + }); + data.splice(1, 0, 'divider'); + return callback(data); + }); + }, + id(obj) { + return obj.id; + }, + text(obj) { + return obj.full_name; + }, + toggleLabel(obj) { + return `${($groupDropdown.data('default-label'))} ${obj.full_name}`; + }, + clicked: () => Search.submitSearch(), + }); + + $projectDropdown.glDropdown({ + selectable: true, + filterable: true, + fieldName: 'project_id', + search: { + fields: ['name'], + }, + data: (term, callback) => { + this.getProjectsData(term) + .then((data) => { data.unshift({ - full_name: 'Any' + name_with_namespace: 'Any', }); data.splice(1, 0, 'divider'); - return callback(data); - }); - }, - id: function(obj) { - return obj.id; - }, - text: function(obj) { - return obj.full_name; - }, - toggleLabel: function(obj) { - return ($groupDropdown.data('default-label')) + " " + obj.full_name; - }, - clicked: (function(_this) { - return function() { - return _this.submitSearch(); - }; - })(this) - }); - $projectDropdown.glDropdown({ - selectable: true, - filterable: true, - fieldName: 'project_id', - search: { - fields: ['name'] - }, - data: (term, callback) => { - this.getProjectsData(term) - .then((data) => { - data.unshift({ - name_with_namespace: 'Any' - }); - data.splice(1, 0, 'divider'); - return data; - }) - .then(data => callback(data)) - .catch(() => new Flash('Error fetching projects')); - }, - id: function(obj) { - return obj.id; - }, - text: function(obj) { - return obj.name_with_namespace; - }, - toggleLabel: function(obj) { - return ($projectDropdown.data('default-label')) + " " + obj.name_with_namespace; - }, - clicked: (function(_this) { - return function() { - return _this.submitSearch(); - }; - })(this) - }); - } + return data; + }) + .then(data => callback(data)) + .catch(() => new Flash('Error fetching projects')); + }, + id(obj) { + return obj.id; + }, + text(obj) { + return obj.name_with_namespace; + }, + toggleLabel(obj) { + return `${($projectDropdown.data('default-label'))} ${obj.name_with_namespace}`; + }, + clicked: () => Search.submitSearch(), + }); + } - Search.prototype.eventListeners = function() { - $(document).off('keyup', '.js-search-input').on('keyup', '.js-search-input', this.searchKeyUp); - return $(document).off('click', '.js-search-clear').on('click', '.js-search-clear', this.clearSearchField); - }; + eventListeners() { + $(document) + .off('keyup', this.searchInput) + .on('keyup', this.searchInput, this.searchKeyUp); + $(document) + .off('click', this.searchClear) + .on('click', this.searchClear, this.clearSearchField.bind(this)); + } - Search.prototype.submitSearch = function() { - return $('.js-search-form').submit(); - }; + static submitSearch() { + return $('.js-search-form').submit(); + } - Search.prototype.searchKeyUp = function() { - var $input; - $input = $(this); - if ($input.val() === '') { - return $('.js-search-clear').addClass('hidden'); - } else { - return $('.js-search-clear').removeClass('hidden'); - } - }; - - Search.prototype.clearSearchField = function() { - return $('.js-search-input').val('').trigger('keyup').focus(); - }; + searchKeyUp() { + const $input = $(this); + if ($input.val() === '') { + $('.js-search-clear').addClass('hidden'); + } else { + $('.js-search-clear').removeClass('hidden'); + } + } - Search.prototype.getProjectsData = function(term) { - return new Promise((resolve) => { - if (this.groupId) { - Api.groupProjects(this.groupId, term, resolve); - } else { - Api.projects(term, { - order_by: 'id', - }, resolve); - } - }); - }; + clearSearchField() { + return $(this.searchInput).val('').trigger('keyup').focus(); + } - return Search; - })(); -}).call(window); + getProjectsData(term) { + return new Promise((resolve) => { + if (this.groupId) { + Api.groupProjects(this.groupId, term, resolve); + } else { + Api.projects(term, { + order_by: 'id', + }, resolve); + } + }); + } +} diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index e40a3596200..98b524f7e3f 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -8,448 +8,445 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. * When the user clicks `x` button it cleans the input and closes the dropdown. */ -((global) => { - const KEYCODE = { - ESCAPE: 27, - BACKSPACE: 8, - ENTER: 13, - UP: 38, - DOWN: 40, - }; - - class SearchAutocomplete { - constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) { - this.bindEventContext(); - this.wrap = wrap || $('.search'); - this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts'); - this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path'); - this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || ''); - this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || ''); - this.dropdown = this.wrap.find('.dropdown'); - this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle'); - this.dropdownContent = this.dropdown.find('.dropdown-content'); - this.locationBadgeEl = this.getElement('.location-badge'); - this.scopeInputEl = this.getElement('#scope'); - this.searchInput = this.getElement('.search-input'); - this.projectInputEl = this.getElement('#search_project_id'); - this.groupInputEl = this.getElement('#group_id'); - this.searchCodeInputEl = this.getElement('#search_code'); - this.repositoryInputEl = this.getElement('#repository_ref'); - this.clearInput = this.getElement('.js-clear-input'); - this.saveOriginalState(); - - // Only when user is logged in - if (gon.current_user_id) { - this.createAutocomplete(); - } +const KEYCODE = { + ESCAPE: 27, + BACKSPACE: 8, + ENTER: 13, + UP: 38, + DOWN: 40, +}; + +function setSearchOptions() { + var $projectOptionsDataEl = $('.js-search-project-options'); + var $groupOptionsDataEl = $('.js-search-group-options'); + var $dashboardOptionsDataEl = $('.js-search-dashboard-options'); + + if ($projectOptionsDataEl.length) { + gl.projectOptions = gl.projectOptions || {}; + + var projectPath = $projectOptionsDataEl.data('project-path'); + + gl.projectOptions[projectPath] = { + name: $projectOptionsDataEl.data('name'), + issuesPath: $projectOptionsDataEl.data('issues-path'), + issuesDisabled: $projectOptionsDataEl.data('issues-disabled'), + mrPath: $projectOptionsDataEl.data('mr-path'), + }; + } - this.searchInput.addClass('disabled'); - this.saveTextLength(); - this.bindEvents(); - this.dropdownToggle.dropdown(); - } + if ($groupOptionsDataEl.length) { + gl.groupOptions = gl.groupOptions || {}; - // Finds an element inside wrapper element - bindEventContext() { - this.onSearchInputBlur = this.onSearchInputBlur.bind(this); - this.onClearInputClick = this.onClearInputClick.bind(this); - this.onSearchInputFocus = this.onSearchInputFocus.bind(this); - this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this); - this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this); - } - getElement(selector) { - return this.wrap.find(selector); - } + var groupPath = $groupOptionsDataEl.data('group-path'); - saveOriginalState() { - return this.originalState = this.serializeState(); - } + gl.groupOptions[groupPath] = { + name: $groupOptionsDataEl.data('name'), + issuesPath: $groupOptionsDataEl.data('issues-path'), + mrPath: $groupOptionsDataEl.data('mr-path'), + }; + } - saveTextLength() { - return this.lastTextLength = this.searchInput.val().length; + if ($dashboardOptionsDataEl.length) { + gl.dashboardOptions = { + issuesPath: $dashboardOptionsDataEl.data('issues-path'), + mrPath: $dashboardOptionsDataEl.data('mr-path'), + }; + } +} + +export default class SearchAutocomplete { + constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) { + setSearchOptions(); + this.bindEventContext(); + this.wrap = wrap || $('.search'); + this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts'); + this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path'); + this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || ''); + this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || ''); + this.dropdown = this.wrap.find('.dropdown'); + this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle'); + this.dropdownContent = this.dropdown.find('.dropdown-content'); + this.locationBadgeEl = this.getElement('.location-badge'); + this.scopeInputEl = this.getElement('#scope'); + this.searchInput = this.getElement('.search-input'); + this.projectInputEl = this.getElement('#search_project_id'); + this.groupInputEl = this.getElement('#group_id'); + this.searchCodeInputEl = this.getElement('#search_code'); + this.repositoryInputEl = this.getElement('#repository_ref'); + this.clearInput = this.getElement('.js-clear-input'); + this.saveOriginalState(); + + // Only when user is logged in + if (gon.current_user_id) { + this.createAutocomplete(); } - createAutocomplete() { - return this.searchInput.glDropdown({ - filterInputBlur: false, - filterable: true, - filterRemote: true, - highlight: true, - enterCallback: false, - filterInput: 'input#search', - search: { - fields: ['text'], - }, - id: this.getSearchText, - data: this.getData.bind(this), - selectable: true, - clicked: this.onClick.bind(this), - }); + this.searchInput.addClass('disabled'); + this.saveTextLength(); + this.bindEvents(); + this.dropdownToggle.dropdown(); + } + + // Finds an element inside wrapper element + bindEventContext() { + this.onSearchInputBlur = this.onSearchInputBlur.bind(this); + this.onClearInputClick = this.onClearInputClick.bind(this); + this.onSearchInputFocus = this.onSearchInputFocus.bind(this); + this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this); + this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this); + } + getElement(selector) { + return this.wrap.find(selector); + } + + saveOriginalState() { + return this.originalState = this.serializeState(); + } + + saveTextLength() { + return this.lastTextLength = this.searchInput.val().length; + } + + createAutocomplete() { + return this.searchInput.glDropdown({ + filterInputBlur: false, + filterable: true, + filterRemote: true, + highlight: true, + enterCallback: false, + filterInput: 'input#search', + search: { + fields: ['text'], + }, + id: this.getSearchText, + data: this.getData.bind(this), + selectable: true, + clicked: this.onClick.bind(this), + }); + } + + getSearchText(selectedObject, el) { + return selectedObject.id ? selectedObject.text : ''; + } + + getData(term, callback) { + if (!term) { + const contents = this.getCategoryContents(); + if (contents) { + this.searchInput.data('glDropdown').filter.options.callback(contents); + this.enableAutocomplete(); + } + return; } - getSearchText(selectedObject, el) { - return selectedObject.id ? selectedObject.text : ''; + // Prevent multiple ajax calls + if (this.loadingSuggestions) { + return; } - getData(term, callback) { - if (!term) { - const contents = this.getCategoryContents(); - if (contents) { - this.searchInput.data('glDropdown').filter.options.callback(contents); - this.enableAutocomplete(); - } - return; - } + this.loadingSuggestions = true; - // Prevent multiple ajax calls - if (this.loadingSuggestions) { + return $.get(this.autocompletePath, { + project_id: this.projectId, + project_ref: this.projectRef, + term: term, + }, (response) => { + var firstCategory, i, lastCategory, len, suggestion; + // Hide dropdown menu if no suggestions returns + if (!response.length) { + this.disableAutocomplete(); return; } - this.loadingSuggestions = true; - - return $.get(this.autocompletePath, { - project_id: this.projectId, - project_ref: this.projectRef, - term: term, - }, (response) => { - var firstCategory, i, lastCategory, len, suggestion; - // Hide dropdown menu if no suggestions returns - if (!response.length) { - this.disableAutocomplete(); - return; - } - - const data = []; - // List results - firstCategory = true; - for (i = 0, len = response.length; i < len; i += 1) { - suggestion = response[i]; - // Add group header before list each group - if (lastCategory !== suggestion.category) { - if (!firstCategory) { - data.push('separator'); - } - if (firstCategory) { - firstCategory = false; - } - data.push({ - header: suggestion.category, - }); - lastCategory = suggestion.category; + const data = []; + // List results + firstCategory = true; + for (i = 0, len = response.length; i < len; i += 1) { + suggestion = response[i]; + // Add group header before list each group + if (lastCategory !== suggestion.category) { + if (!firstCategory) { + data.push('separator'); + } + if (firstCategory) { + firstCategory = false; } data.push({ - id: (suggestion.category.toLowerCase()) + "-" + suggestion.id, - category: suggestion.category, - text: suggestion.label, - url: suggestion.url, - }); - } - // Add option to proceed with the search - if (data.length) { - data.push('separator'); - data.push({ - text: "Result name contains \"" + term + "\"", - url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()), + header: suggestion.category, }); + lastCategory = suggestion.category; } - return callback(data); - }) - .always(() => { this.loadingSuggestions = false; }); - } - - getCategoryContents() { - const userId = gon.current_user_id; - const userName = gon.current_username; - const { projectOptions, groupOptions, dashboardOptions } = gl; - - // Get options - let options; - if (isInGroupsPage() && groupOptions) { - options = groupOptions[getGroupSlug()]; - } else if (isInProjectPage() && projectOptions) { - options = projectOptions[getProjectSlug()]; - } else if (dashboardOptions) { - options = dashboardOptions; + data.push({ + id: (suggestion.category.toLowerCase()) + "-" + suggestion.id, + category: suggestion.category, + text: suggestion.label, + url: suggestion.url, + }); } - - const { issuesPath, mrPath, name, issuesDisabled } = options; - const baseItems = []; - - if (name) { - baseItems.push({ - header: `${name}`, + // Add option to proceed with the search + if (data.length) { + data.push('separator'); + data.push({ + text: "Result name contains \"" + term + "\"", + url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()), }); } + return callback(data); + }) + .always(() => { this.loadingSuggestions = false; }); + } - const issueItems = [ - { - text: 'Issues assigned to me', - url: `${issuesPath}/?assignee_username=${userName}`, - }, - { - text: "Issues I've created", - url: `${issuesPath}/?author_username=${userName}`, - }, - ]; - const mergeRequestItems = [ - { - text: 'Merge requests assigned to me', - url: `${mrPath}/?assignee_username=${userName}`, - }, - { - text: "Merge requests I've created", - url: `${mrPath}/?author_username=${userName}`, - }, - ]; - - let items; - if (issuesDisabled) { - items = baseItems.concat(mergeRequestItems); - } else { - items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems); - } - return items; + getCategoryContents() { + const userId = gon.current_user_id; + const userName = gon.current_username; + const { projectOptions, groupOptions, dashboardOptions } = gl; + + // Get options + let options; + if (isInGroupsPage() && groupOptions) { + options = groupOptions[getGroupSlug()]; + } else if (isInProjectPage() && projectOptions) { + options = projectOptions[getProjectSlug()]; + } else if (dashboardOptions) { + options = dashboardOptions; } - serializeState() { - return { - // Search Criteria - search_project_id: this.projectInputEl.val(), - group_id: this.groupInputEl.val(), - search_code: this.searchCodeInputEl.val(), - repository_ref: this.repositoryInputEl.val(), - scope: this.scopeInputEl.val(), - // Location badge - _location: this.locationBadgeEl.text(), - }; + const { issuesPath, mrPath, name, issuesDisabled } = options; + const baseItems = []; + + if (name) { + baseItems.push({ + header: `${name}`, + }); } - bindEvents() { - this.searchInput.on('keydown', this.onSearchInputKeyDown); - this.searchInput.on('keyup', this.onSearchInputKeyUp); - this.searchInput.on('focus', this.onSearchInputFocus); - this.searchInput.on('blur', this.onSearchInputBlur); - this.clearInput.on('click', this.onClearInputClick); - this.locationBadgeEl.on('click', () => this.searchInput.focus()); + const issueItems = [ + { + text: 'Issues assigned to me', + url: `${issuesPath}/?assignee_username=${userName}`, + }, + { + text: "Issues I've created", + url: `${issuesPath}/?author_username=${userName}`, + }, + ]; + const mergeRequestItems = [ + { + text: 'Merge requests assigned to me', + url: `${mrPath}/?assignee_username=${userName}`, + }, + { + text: "Merge requests I've created", + url: `${mrPath}/?author_username=${userName}`, + }, + ]; + + let items; + if (issuesDisabled) { + items = baseItems.concat(mergeRequestItems); + } else { + items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems); } + return items; + } - enableAutocomplete() { - // No need to enable anything if user is not logged in - if (!gon.current_user_id) { - return; - } + serializeState() { + return { + // Search Criteria + search_project_id: this.projectInputEl.val(), + group_id: this.groupInputEl.val(), + search_code: this.searchCodeInputEl.val(), + repository_ref: this.repositoryInputEl.val(), + scope: this.scopeInputEl.val(), + // Location badge + _location: this.locationBadgeEl.text(), + }; + } - // If the dropdown is closed, we'll open it - if (!this.dropdown.hasClass('open')) { - this.loadingSuggestions = false; - this.dropdownToggle.dropdown('toggle'); - return this.searchInput.removeClass('disabled'); - } + bindEvents() { + this.searchInput.on('keydown', this.onSearchInputKeyDown); + this.searchInput.on('keyup', this.onSearchInputKeyUp); + this.searchInput.on('focus', this.onSearchInputFocus); + this.searchInput.on('blur', this.onSearchInputBlur); + this.clearInput.on('click', this.onClearInputClick); + this.locationBadgeEl.on('click', () => this.searchInput.focus()); + } + + enableAutocomplete() { + // No need to enable anything if user is not logged in + if (!gon.current_user_id) { + return; } - // Saves last length of the entered text - onSearchInputKeyDown() { - return this.saveTextLength(); + // If the dropdown is closed, we'll open it + if (!this.dropdown.hasClass('open')) { + this.loadingSuggestions = false; + this.dropdownToggle.dropdown('toggle'); + return this.searchInput.removeClass('disabled'); } + } - onSearchInputKeyUp(e) { - switch (e.keyCode) { - case KEYCODE.BACKSPACE: - // when trying to remove the location badge - if (this.lastTextLength === 0 && this.badgePresent()) { - this.removeLocationBadge(); - } - // When removing the last character and no badge is present - if (this.lastTextLength === 1) { - this.disableAutocomplete(); - } - // When removing any character from existin value - if (this.lastTextLength > 1) { - this.enableAutocomplete(); - } - break; - case KEYCODE.ESCAPE: - this.restoreOriginalState(); - break; - case KEYCODE.ENTER: + // Saves last length of the entered text + onSearchInputKeyDown() { + return this.saveTextLength(); + } + + onSearchInputKeyUp(e) { + switch (e.keyCode) { + case KEYCODE.BACKSPACE: + // when trying to remove the location badge + if (this.lastTextLength === 0 && this.badgePresent()) { + this.removeLocationBadge(); + } + // When removing the last character and no badge is present + if (this.lastTextLength === 1) { + this.disableAutocomplete(); + } + // When removing any character from existin value + if (this.lastTextLength > 1) { + this.enableAutocomplete(); + } + break; + case KEYCODE.ESCAPE: + this.restoreOriginalState(); + break; + case KEYCODE.ENTER: + this.disableAutocomplete(); + break; + case KEYCODE.UP: + case KEYCODE.DOWN: + return; + default: + // Handle the case when deleting the input value other than backspace + // e.g. Pressing ctrl + backspace or ctrl + x + if (this.searchInput.val() === '') { this.disableAutocomplete(); - break; - case KEYCODE.UP: - case KEYCODE.DOWN: - return; - default: - // Handle the case when deleting the input value other than backspace - // e.g. Pressing ctrl + backspace or ctrl + x - if (this.searchInput.val() === '') { - this.disableAutocomplete(); - } else { - // We should display the menu only when input is not empty - if (e.keyCode !== KEYCODE.ENTER) { - this.enableAutocomplete(); - } + } else { + // We should display the menu only when input is not empty + if (e.keyCode !== KEYCODE.ENTER) { + this.enableAutocomplete(); } - } - this.wrap.toggleClass('has-value', !!e.target.value); + } } + this.wrap.toggleClass('has-value', !!e.target.value); + } - onSearchInputFocus() { - this.isFocused = true; - this.wrap.addClass('search-active'); - if (this.getValue() === '') { - return this.getData(); - } + onSearchInputFocus() { + this.isFocused = true; + this.wrap.addClass('search-active'); + if (this.getValue() === '') { + return this.getData(); } + } - getValue() { - return this.searchInput.val(); - } + getValue() { + return this.searchInput.val(); + } - onClearInputClick(e) { - e.preventDefault(); - this.wrap.toggleClass('has-value', !!e.target.value); - return this.searchInput.val('').focus(); - } + onClearInputClick(e) { + e.preventDefault(); + this.wrap.toggleClass('has-value', !!e.target.value); + return this.searchInput.val('').focus(); + } - onSearchInputBlur(e) { - this.isFocused = false; - this.wrap.removeClass('search-active'); - // If input is blank then restore state - if (this.searchInput.val() === '') { - return this.restoreOriginalState(); - } + onSearchInputBlur(e) { + this.isFocused = false; + this.wrap.removeClass('search-active'); + // If input is blank then restore state + if (this.searchInput.val() === '') { + return this.restoreOriginalState(); } + } - addLocationBadge(item) { - var badgeText, category, value; - category = item.category != null ? item.category + ": " : ''; - value = item.value != null ? item.value : ''; - badgeText = "" + category + value; - this.locationBadgeEl.text(badgeText).show(); - return this.wrap.addClass('has-location-badge'); - } + addLocationBadge(item) { + var badgeText, category, value; + category = item.category != null ? item.category + ": " : ''; + value = item.value != null ? item.value : ''; + badgeText = "" + category + value; + this.locationBadgeEl.text(badgeText).show(); + return this.wrap.addClass('has-location-badge'); + } - hasLocationBadge() { - return this.wrap.is('.has-location-badge'); - } + hasLocationBadge() { + return this.wrap.is('.has-location-badge'); + } - restoreOriginalState() { - var i, input, inputs, len; - inputs = Object.keys(this.originalState); - for (i = 0, len = inputs.length; i < len; i += 1) { - input = inputs[i]; - this.getElement("#" + input).val(this.originalState[input]); - } - if (this.originalState._location === '') { - return this.locationBadgeEl.hide(); - } else { - return this.addLocationBadge({ - value: this.originalState._location, - }); - } + restoreOriginalState() { + var i, input, inputs, len; + inputs = Object.keys(this.originalState); + for (i = 0, len = inputs.length; i < len; i += 1) { + input = inputs[i]; + this.getElement("#" + input).val(this.originalState[input]); } - - badgePresent() { - return this.locationBadgeEl.length; + if (this.originalState._location === '') { + return this.locationBadgeEl.hide(); + } else { + return this.addLocationBadge({ + value: this.originalState._location, + }); } + } - resetSearchState() { - var i, input, inputs, len, results; - inputs = Object.keys(this.originalState); - results = []; - for (i = 0, len = inputs.length; i < len; i += 1) { - input = inputs[i]; - // _location isnt a input - if (input === '_location') { - break; - } - results.push(this.getElement("#" + input).val('')); + badgePresent() { + return this.locationBadgeEl.length; + } + + resetSearchState() { + var i, input, inputs, len, results; + inputs = Object.keys(this.originalState); + results = []; + for (i = 0, len = inputs.length; i < len; i += 1) { + input = inputs[i]; + // _location isnt a input + if (input === '_location') { + break; } - return results; + results.push(this.getElement("#" + input).val('')); } + return results; + } - removeLocationBadge() { - this.locationBadgeEl.hide(); - this.resetSearchState(); - this.wrap.removeClass('has-location-badge'); - return this.disableAutocomplete(); - } + removeLocationBadge() { + this.locationBadgeEl.hide(); + this.resetSearchState(); + this.wrap.removeClass('has-location-badge'); + return this.disableAutocomplete(); + } - disableAutocomplete() { - if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) { - this.searchInput.addClass('disabled'); - this.dropdown.removeClass('open').trigger('hidden.bs.dropdown'); - this.restoreMenu(); - } + disableAutocomplete() { + if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) { + this.searchInput.addClass('disabled'); + this.dropdown.removeClass('open').trigger('hidden.bs.dropdown'); + this.restoreMenu(); } + } - restoreMenu() { - var html; - html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>'; - return this.dropdownContent.html(html); - } + restoreMenu() { + var html; + html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>'; + return this.dropdownContent.html(html); + } - onClick(item, $el, e) { - if (location.pathname.indexOf(item.url) !== -1) { - if (!e.metaKey) e.preventDefault(); - if (!this.badgePresent) { - if (item.category === 'Projects') { - this.projectInputEl.val(item.id); - this.addLocationBadge({ - value: 'This project', - }); - } - if (item.category === 'Groups') { - this.groupInputEl.val(item.id); - this.addLocationBadge({ - value: 'This group', - }); - } + onClick(item, $el, e) { + if (location.pathname.indexOf(item.url) !== -1) { + if (!e.metaKey) e.preventDefault(); + if (!this.badgePresent) { + if (item.category === 'Projects') { + this.projectInputEl.val(item.id); + this.addLocationBadge({ + value: 'This project', + }); + } + if (item.category === 'Groups') { + this.groupInputEl.val(item.id); + this.addLocationBadge({ + value: 'This group', + }); } - $el.removeClass('is-active'); - this.disableAutocomplete(); - return this.searchInput.val('').focus(); } + $el.removeClass('is-active'); + this.disableAutocomplete(); + return this.searchInput.val('').focus(); } } - - global.SearchAutocomplete = SearchAutocomplete; - - $(function() { - var $projectOptionsDataEl = $('.js-search-project-options'); - var $groupOptionsDataEl = $('.js-search-group-options'); - var $dashboardOptionsDataEl = $('.js-search-dashboard-options'); - - if ($projectOptionsDataEl.length) { - gl.projectOptions = gl.projectOptions || {}; - - var projectPath = $projectOptionsDataEl.data('project-path'); - - gl.projectOptions[projectPath] = { - name: $projectOptionsDataEl.data('name'), - issuesPath: $projectOptionsDataEl.data('issues-path'), - issuesDisabled: $projectOptionsDataEl.data('issues-disabled'), - mrPath: $projectOptionsDataEl.data('mr-path'), - }; - } - - if ($groupOptionsDataEl.length) { - gl.groupOptions = gl.groupOptions || {}; - - var groupPath = $groupOptionsDataEl.data('group-path'); - - gl.groupOptions[groupPath] = { - name: $groupOptionsDataEl.data('name'), - issuesPath: $groupOptionsDataEl.data('issues-path'), - mrPath: $groupOptionsDataEl.data('mr-path'), - }; - } - - if ($dashboardOptionsDataEl.length) { - gl.dashboardOptions = { - issuesPath: $dashboardOptionsDataEl.data('issues-path'), - mrPath: $dashboardOptionsDataEl.data('mr-path'), - }; - } - }); -})(window.gl || (window.gl = {})); +} diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 3f811c59cb9..95e51bc4e7a 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -2,6 +2,7 @@ import FilesCommentButton from './files_comment_button'; import imageDiffHelper from './image_diff/helpers/index'; +import syntaxHighlight from './syntax_highlight'; const WRAPPER = '<div class="diff-content"></div>'; const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; @@ -64,7 +65,7 @@ export default class SingleFileDiff { _this.loadingContent.hide(); if (data.html) { _this.content = $(data.html); - _this.content.syntaxHighlight(); + syntaxHighlight(_this.content); } else { _this.hasError = true; _this.content = $(ERROR_HTML); diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index 662d6b36c16..62bdef76c55 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -10,17 +10,15 @@ // <div class="js-syntax-highlight"></div> // -$.fn.syntaxHighlight = function() { - var $children; - - if ($(this).hasClass('js-syntax-highlight')) { +export default function syntaxHighlight(el) { + if ($(el).hasClass('js-syntax-highlight')) { // Given the element itself, apply highlighting - return $(this).addClass(gon.user_color_scheme); + return $(el).addClass(gon.user_color_scheme); } else { // Given a parent element, recurse to any of its applicable children - $children = $(this).find('.js-syntax-highlight'); + const $children = $(el).find('.js-syntax-highlight'); if ($children.length) { - return $children.syntaxHighlight(); + return syntaxHighlight($children); } } -}; +} diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js index 5e947769f8a..4fa8c680580 100644 --- a/app/assets/javascripts/users/activity_calendar.js +++ b/app/assets/javascripts/users/activity_calendar.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import d3 from 'd3'; +import { getDayName, getDayDifference } from '../lib/utils/datetime_utility'; const LOADING_HTML = ` <div class="text-center"> @@ -17,7 +18,7 @@ function getSystemDate(systemUtcOffsetSeconds) { function formatTooltipText({ date, count }) { const dateObject = new Date(date); - const dateDayName = gl.utils.getDayName(dateObject); + const dateDayName = getDayName(dateObject); const dateText = dateObject.format('mmm d, yyyy'); let contribText = 'No contributions'; @@ -51,7 +52,7 @@ export default class ActivityCalendar { const oneYearAgo = new Date(today); oneYearAgo.setFullYear(today.getFullYear() - 1); - const days = gl.utils.getDayDifference(oneYearAgo, today); + const days = getDayDifference(oneYearAgo, today); for (let i = 0; i <= days; i += 1) { const date = new Date(oneYearAgo); diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js index 1215b265e28..992baa9a1ef 100644 --- a/app/assets/javascripts/users/user_tabs.js +++ b/app/assets/javascripts/users/user_tabs.js @@ -1,4 +1,6 @@ +import Activities from '../activities'; import ActivityCalendar from './activity_calendar'; +import { localTimeAgo } from '../lib/utils/datetime_utility'; /** * UserTabs @@ -138,7 +140,7 @@ export default class UserTabs { const tabSelector = `div#${action}`; this.$parentEl.find(tabSelector).html(data.html); this.loaded[action] = true; - gl.utils.localTimeAgo($('.js-timeago', tabSelector)); + localTimeAgo($('.js-timeago', tabSelector)); }, }); } @@ -169,7 +171,7 @@ export default class UserTabs { }); // eslint-disable-next-line no-new - new gl.Activities(); + new Activities(); this.loaded.activity = true; } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index 32028a4a609..ee1a45cc754 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -1,4 +1,4 @@ -import '~/lib/utils/datetime_utility'; +import { getTimeago } from '~/lib/utils/datetime_utility'; import { visitUrl } from '../../lib/utils/url_utility'; import Flash from '../../flash'; import MemoryUsage from './mr_widget_memory_usage'; @@ -17,7 +17,7 @@ export default { }, methods: { formatDate(date) { - return gl.utils.getTimeago().format(date); + return getTimeago().format(date); }, hasExternalUrls(deployment = {}) { return deployment.external_url && deployment.external_url_formatted; 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 c1f7e64f580..707766e08e4 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 @@ -1,5 +1,6 @@ import Timeago from 'timeago.js'; import { getStateKey } from '../dependencies'; +import { formatDate } from '../../lib/utils/datetime_utility'; export default class MergeRequestStore { @@ -122,7 +123,7 @@ export default class MergeRequestStore { static getEventObject(event) { return { author: MergeRequestStore.getAuthorObject(event), - updatedAt: gl.utils.formatDate(MergeRequestStore.getEventUpdatedAtDate(event)), + updatedAt: formatDate(MergeRequestStore.getEventUpdatedAtDate(event)), formattedUpdatedAt: MergeRequestStore.getEventDate(event), }; } diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.js b/app/assets/javascripts/vue_shared/components/memory_graph.js index 643b77e04c7..f37ef1a5ca3 100644 --- a/app/assets/javascripts/vue_shared/components/memory_graph.js +++ b/app/assets/javascripts/vue_shared/components/memory_graph.js @@ -1,3 +1,5 @@ +import { getTimeago } from '../../lib/utils/datetime_utility'; + export default { name: 'MemoryGraph', props: { @@ -16,7 +18,7 @@ export default { }, computed: { getFormattedMedian() { - const deployedSince = gl.utils.getTimeago().format(this.deploymentTime * 1000); + const deployedSince = getTimeago().format(this.deploymentTime * 1000); return `Deployed ${deployedSince}`; }, }, diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/modal.vue index 6d15bbd84ba..55f466b7b41 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/modal.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'popup-dialog', + name: 'modal', props: { title: { @@ -75,7 +75,7 @@ export default { <template> <div class="modal-open"> <div - class="modal popup-dialog" + class="modal show" role="dialog" tabindex="-1" > diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_dialog.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 3ec50f14eb4..8053c65d498 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -1,8 +1,8 @@ <script> -import PopupDialog from './popup_dialog.vue'; +import modal from './modal.vue'; export default { - name: 'recaptcha-dialog', + name: 'recaptcha-modal', props: { html: { @@ -20,7 +20,7 @@ export default { }, components: { - PopupDialog, + modal, }, methods: { @@ -65,9 +65,9 @@ export default { </script> <template> -<popup-dialog +<modal kind="warning" - class="recaptcha-dialog js-recaptcha-dialog" + class="recaptcha-modal js-recaptcha-modal" :hide-footer="true" :title="__('Please solve the reCAPTCHA')" @toggle="close" @@ -81,5 +81,5 @@ export default { v-html="html" ></div> </div> -</popup-dialog> +</modal> </template> diff --git a/app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js b/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js index ef70f9432e3..ff1f565e79a 100644 --- a/app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js +++ b/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js @@ -1,4 +1,4 @@ -import RecaptchaDialog from '../components/recaptcha_dialog.vue'; +import recaptchaModal from '../components/recaptcha_modal.vue'; export default { data() { @@ -9,7 +9,7 @@ export default { }, components: { - RecaptchaDialog, + recaptchaModal, }, methods: { diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js index 20f63ab663c..4e3b9d7b767 100644 --- a/app/assets/javascripts/vue_shared/mixins/timeago.js +++ b/app/assets/javascripts/vue_shared/mixins/timeago.js @@ -1,4 +1,4 @@ -import '../../lib/utils/datetime_utility'; +import { formatDate, getTimeago } from '../../lib/utils/datetime_utility'; /** * Mixin with time ago methods used in some vue components @@ -6,13 +6,13 @@ import '../../lib/utils/datetime_utility'; export default { methods: { timeFormated(time) { - const timeago = gl.utils.getTimeago(); + const timeago = getTimeago(); return timeago.format(time); }, tooltipTitle(time) { - return gl.utils.formatDate(time); + return formatDate(time); }, }, }; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 8d83554d813..478269f3fcf 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -143,20 +143,48 @@ } } +@mixin dropdown-item-hover { + background-color: $dropdown-item-hover-bg; + color: $gl-text-color; + outline: 0; + + // make sure the text color is not overriden + &.text-danger { + color: $brand-danger; + } + + .avatar { + border-color: $white-light; + } +} + @mixin dropdown-link { + background: transparent; + border: 0; + border-radius: 0; + box-shadow: none; display: block; + font-weight: $gl-font-weight-normal; position: relative; - padding: 5px 8px; + padding: 8px 16px; color: $gl-text-color; - line-height: initial; - border-radius: 2px; - white-space: nowrap; + line-height: normal; + white-space: normal; overflow: hidden; + text-align: left; + width: 100%; + + // make sure the text color is not overriden + &.text-danger { + color: $brand-danger; + } &:hover, + &:active, &:focus, &.is-focused { - background-color: $dropdown-link-hover-bg; + @include dropdown-item-hover; + text-decoration: none; .badge { @@ -166,6 +194,13 @@ &.dropdown-menu-user-link { line-height: 16px; + padding-top: 10px; + padding-bottom: 7px; + white-space: nowrap; + + .dropdown-menu-user-username { + display: block; + } } .icon-play { @@ -187,8 +222,8 @@ z-index: 300; min-width: 240px; max-width: 500px; - margin-top: 2px; - margin-bottom: 2px; + margin-top: $dropdown-vertical-offset; + margin-bottom: 24px; font-size: 14px; font-weight: $gl-font-weight-normal; padding: 8px 0; @@ -197,6 +232,10 @@ border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; + &.dropdown-open-top { + margin-bottom: $dropdown-vertical-offset; + } + &.dropdown-open-left { right: 0; left: auto; @@ -227,16 +266,27 @@ } li { + display: block; text-align: left; list-style: none; - padding: 0 10px; + padding: 0 1px; + + a, + button, + .menu-item { + @include dropdown-link; + } } .divider { height: 1px; - margin: 6px 10px; + margin: 6px 0; padding: 0; background-color: $dropdown-divider-color; + + &:hover { + background-color: $dropdown-divider-color; + } } .separator { @@ -247,10 +297,6 @@ background-color: $dropdown-divider-color; } - a { - @include dropdown-link; - } - .dropdown-menu-empty-item a { &:hover, &:focus { @@ -262,7 +308,7 @@ color: $gl-text-color-secondary; font-size: 13px; line-height: 22px; - padding: 0 16px; + padding: 8px 16px; } &.capitalize-header .dropdown-header { @@ -277,7 +323,7 @@ .separator + .dropdown-header, .separator + .dropdown-bold-header { - padding-top: 2px; + padding-top: 10px; } .unclickable { @@ -298,48 +344,28 @@ } .dropdown-menu li { - padding: $gl-btn-padding; cursor: pointer; + &.droplab-item-active button { + @include dropdown-item-hover; + } + > a, > button { display: flex; margin: 0; - padding: 0; - border-radius: 0; text-overflow: inherit; - background-color: inherit; - color: inherit; - border: inherit; text-align: left; - &:hover, - &:focus { - background-color: inherit; - color: inherit; - } - &.btn .fa:not(:last-child) { margin-left: 5px; } } - &:hover, - &:focus { - background-color: $dropdown-hover-color; - color: $white-light; - } - &.droplab-item-selected i { visibility: visible; } - &.divider { - margin: 0 8px; - padding: 0; - border-top: $gray-darkest; - } - .icon { visibility: hidden; } @@ -431,11 +457,6 @@ } } -.dropdown-menu-user-link { - padding-top: 10px; - padding-bottom: 7px; -} - .dropdown-menu-user-full-name { display: block; font-weight: $gl-font-weight-normal; @@ -464,41 +485,44 @@ .dropdown-menu-align-right { left: auto; right: 0; - margin-top: -5px; } .dropdown-menu-selectable { - a { - padding-left: 26px; - position: relative; + li { + a { + padding: 8px 40px; + position: relative; + + &.is-indeterminate, + &.is-active { + color: $gl-text-color; + + &::before { + position: absolute; + left: 16px; + top: 16px; + transform: translateY(-50%); + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } - &.is-indeterminate, - &.is-active { - font-weight: $gl-font-weight-bold; - color: $gl-text-color; - - &::before { - position: absolute; - left: 6px; - top: 50%; - transform: translateY(-50%); - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + &.dropdown-menu-user-link { + &::before { + top: 50%; + } + } } - } - &.is-indeterminate::before { - content: "\f068"; - } + &.is-indeterminate::before { + content: "\f068"; + } - &.is-active::before { - content: "\f00c"; - position: absolute; - top: 50%; - transform: translateY(-50%); + &.is-active::before { + content: "\f00c"; + } } } } @@ -735,136 +759,6 @@ } } -@mixin dropdown-item-hover { - background-color: $dropdown-item-hover-bg; - color: $gl-text-color; -} - -// TODO: change global style and remove mixin -@mixin new-style-dropdown($selector: '') { - #{$selector}.dropdown-menu, - #{$selector}.dropdown-menu-nav { - margin-bottom: 24px; - - &.dropdown-open-top { - margin-bottom: $dropdown-vertical-offset; - } - - li { - display: block; - padding: 0 1px; - - &:hover { - background-color: transparent; - } - - &.divider { - margin: 6px 0; - - &:hover { - background-color: $dropdown-divider-color; - } - } - - &.dropdown-header { - padding: 8px 16px; - } - - &.droplab-item-active button { - @include dropdown-item-hover; - } - - a, - button, - .menu-item { - margin-bottom: 0; - border-radius: 0; - box-shadow: none; - padding: 8px 16px; - text-align: left; - white-space: normal; - width: 100%; - font-weight: $gl-font-weight-normal; - line-height: normal; - - &.dropdown-menu-user-link { - white-space: nowrap; - - .dropdown-menu-user-username { - display: block; - } - } - - // make sure the text color is not overriden - &.text-danger { - color: $brand-danger; - } - - &.is-focused, - &:hover, - &:active, - &:focus { - @include dropdown-item-hover; - - background-color: $dropdown-item-hover-bg; - color: $gl-text-color; - - // make sure the text color is not overriden - &.text-danger { - color: $brand-danger; - } - } - - &.is-active { - font-weight: inherit; - - &::before { - top: 16px; - } - - &.dropdown-menu-user-link::before { - top: 50%; - transform: translateY(-50%); - } - } - } - - &.dropdown-menu-empty-item a { - &:hover, - &:focus { - background-color: transparent; - } - } - } - - &.dropdown-menu-selectable { - li { - a { - padding: 8px 40px; - - &.is-indeterminate::before, - &.is-active::before { - left: 16px; - } - } - } - } - } - - #{$selector}.dropdown-menu-align-right { - margin-top: 2px; - } - - .open { - #{$selector}.dropdown-menu, - #{$selector}.dropdown-menu-nav { - @media (max-width: $screen-xs-max) { - max-width: 100%; - } - } - } -} - @media (max-width: $screen-xs-max) { .navbar-gitlab { li.header-projects, @@ -891,9 +785,6 @@ } } -@include new-style-dropdown('.breadcrumbs-list .dropdown '); -@include new-style-dropdown('.js-namespace-select + '); - header.header-content .dropdown-menu.projects-dropdown-menu { padding: 0; } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index cec38eea464..2d7465401f1 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -50,8 +50,6 @@ } .filtered-search-wrapper { - @include new-style-dropdown; - display: -webkit-flex; display: flex; @@ -165,16 +163,6 @@ } } -.droplab-dropdown li.filtered-search-token { - padding: 0; - - &:hover, - &:focus { - background-color: inherit; - color: inherit; - } -} - .filtered-search-term { .name { background-color: inherit; @@ -336,21 +324,12 @@ .filtered-search-history-dropdown-content { max-height: none; -} - -.filtered-search-history-dropdown-item, -.filtered-search-history-clear-button { - @include dropdown-link; - - overflow: hidden; - width: 100%; - margin: 0.5em 0; - background-color: transparent; - border: 0; - text-align: left; - white-space: nowrap; - text-overflow: ellipsis; + .filtered-search-history-dropdown-item, + .filtered-search-history-clear-button { + white-space: nowrap; + text-overflow: ellipsis; + } } .filtered-search-history-dropdown-token { @@ -402,24 +381,9 @@ } } -%filter-dropdown-item-btn-hover { - text-decoration: none; - outline: 0; - - .avatar { - border-color: $white-light; - } -} - .droplab-dropdown .dropdown-menu .filter-dropdown-item { .btn { - border: 0; - width: 100%; - text-align: left; - padding: 8px 16px; text-overflow: ellipsis; - overflow: hidden; - border-radius: 0; .fa { width: 15px; @@ -434,11 +398,6 @@ height: 17px; top: 0; } - - &:hover, - &:focus { - @extend %filter-dropdown-item-btn-hover; - } } .dropdown-light-content { @@ -459,17 +418,9 @@ word-break: break-all; } } - - &.droplab-item-active .btn { - @extend %filter-dropdown-item-btn-hover; - } } .filter-dropdown-loading { padding: 8px 16px; text-align: center; } - -.issues-details-filters { - @include new-style-dropdown; -} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index f985a3aea5c..29714e348a0 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -1,10 +1,4 @@ -.content-wrapper.page-with-new-nav { - margin-top: $header-height; -} - .navbar-gitlab { - @include new-style-dropdown; - &.navbar-gitlab { padding: 0 16px; z-index: 1000; diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 78a8e57ddbb..aa2d30a3cef 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -19,6 +19,13 @@ max-width: 425px; width: 100%; } + + &.svg-250 { + img, + svg { + width: 250px; + } + } } @mixin svg-size($size) { diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 1537b0744cc..1d8bd26cf1a 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -24,10 +24,14 @@ font-size: $gl-font-size; line-height: 25px; - &.status-box-closed { + &.status-box-mr-closed { background-color: $gl-danger; } + &.status-box-issue-closed { + background-color: $gl-primary; + } + &.status-box-merged { background-color: $gl-primary; } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index cb324ccc440..3f0268541a4 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -24,6 +24,7 @@ body { } .content-wrapper { + margin-top: $header-height; padding-bottom: 100px; } @@ -105,11 +106,11 @@ body { } } -.page-with-sidebar > .content-wrapper { +.layout-page > .content-wrapper { min-height: calc(100vh - #{$header-height}); } -.with-performance-bar .page-with-sidebar { +.with-performance-bar .layout-page { margin-top: $header-height + $performance-bar-height; } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index e6e6c4c3963..f79a71221c4 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -132,8 +132,6 @@ ul.content-list { } .controls { - @include new-style-dropdown; - float: right; > .control-text { diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 600a1f53b58..a12f28efce6 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -111,21 +111,4 @@ aside:not(.right-sidebar) { display: none; } - - .show-aside { - display: block !important; - } -} - -.show-aside { - display: none; - position: fixed; - right: 0; - top: 30%; - padding: 5px 15px; - background: $show-aside-bg; - font-size: 20px; - color: $show-aside-color; - z-index: 100; - box-shadow: 0 1px 2px $show-aside-shadow; } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index ce551e6b7ce..1be66d0ab21 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -44,11 +44,18 @@ body.modal-open { } } -.modal.popup-dialog { - display: block; +.modal { + background-color: $black-transparent; + z-index: 2100; + + @media (min-width: $screen-md-min) { + .modal-dialog { + margin: 30px auto; + } + } } -.recaptcha-dialog .recaptcha-form { +.recaptcha-modal .recaptcha-form { display: inline-block; .recaptcha { diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss index 8498b37abe4..5f67126bafa 100644 --- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss +++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss @@ -86,8 +86,6 @@ } .nav-controls { - @include new-style-dropdown; - display: inline-block; float: right; text-align: right; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 3ebba4f9efb..0742c0a2a09 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -144,10 +144,6 @@ } } -.issuable-sidebar { - @include new-style-dropdown; -} - .pikaday-container { .pika-single { margin-top: 2px; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 0817cce114c..11c1aeea871 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -343,8 +343,6 @@ a > code { @extend .ref-name; } -@include new-style-dropdown('.git-revision-dropdown'); - /** * Apply Markdown typography * diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 5de5403916f..b84d6c140be 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -245,9 +245,6 @@ $btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; $issue-status-expired: $orange-500; $issuable-sidebar-color: $gl-text-color-secondary; -$show-aside-bg: #eee; -$show-aside-color: #777; -$show-aside-shadow: #ddd; $group-path-color: #999; $namespace-kind-color: #aaa; $panel-heading-link-color: #777; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 862ea379cbc..2803144ef1d 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -415,7 +415,7 @@ margin: 5px; } -.page-with-contextual-sidebar.page-with-sidebar .issue-boards-sidebar { +.page-with-contextual-sidebar.layout-page .issue-boards-sidebar { .issuable-sidebar-header { position: relative; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index f139f4ab650..98d460339cd 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -323,8 +323,6 @@ } .build-dropdown { - @include new-style-dropdown; - margin: $gl-padding 0; padding: 0; diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index c303f016ff9..88d44131d5b 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -13,8 +13,6 @@ max-width: 100%; } -@include new-style-dropdown('.clusters-dropdown '); - .clusters-container { .nav-bar-right { padding: $gl-padding-top $gl-padding; diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 292e0ad394b..3b35beb7695 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -1,6 +1,4 @@ #cycle-analytics { - @include new-style-dropdown; - max-width: 1000px; margin: 24px auto 0; position: relative; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 52e4d904b9b..2f2c04206e2 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -32,8 +32,6 @@ } .detail-page-header-actions { - @include new-style-dropdown; - align-self: center; flex-shrink: 0; flex: 0 0 auto; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 71a6c7a2bf9..60b07537799 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -581,8 +581,6 @@ } .commit-stat-summary { - @include new-style-dropdown; - @media (min-width: $screen-sm-min) { margin-left: -$gl-padding; padding-left: $gl-padding; diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index c586dab4cf2..8ecda50602d 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -204,8 +204,6 @@ .gitlab-ci-yml-selector, .dockerfile-selector, .template-type-selector { - @include new-style-dropdown; - display: inline-block; vertical-align: top; font-family: $regular_font; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index a5a6b7461a3..f4882305c57 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -12,8 +12,6 @@ .environments-container { .ci-table { - @include new-style-dropdown; - .deployment-column { > span { word-break: break-all; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 2e26f2b5bbd..e19196e0c41 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -488,12 +488,6 @@ } } - .dropdown-content { - a:hover { - color: inherit; - } - } - .dropdown-menu-toggle { width: 100%; padding-top: 6px; @@ -512,10 +506,6 @@ } } -.sidebar-move-issue-dropdown { - @include new-style-dropdown; -} - .sidebar-move-issue-confirmation-button { width: 100%; @@ -620,11 +610,19 @@ } .issuable-status-box { - float: none; - display: inline-block; + align-self: stretch; + display: flex; + justify-content: center; + align-items: center; margin-top: 0; - height: auto; - align-self: center; + padding-left: 9px; + padding-right: 9px; + + @media (min-width: $screen-sm-min) { + display: inline-block; + height: auto; + align-self: center; + } } .issuable-gutter-toggle { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index af1df8b8802..c48e58af691 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -142,8 +142,6 @@ ul.related-merge-requests > li { } .issue-form { - @include new-style-dropdown; - .select2-container { width: 250px !important; } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 92abe82df4c..e8cd8a4905c 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -116,8 +116,6 @@ } .manage-labels-list { - @include new-style-dropdown; - > li:not(.empty-message):not(.is-not-draggable) { background-color: $white-light; cursor: move; diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 18c48405ecd..3422829de58 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -58,8 +58,6 @@ } .member-form-control { - @include new-style-dropdown; - @media (max-width: $screen-xs-max) { padding-bottom: 5px; margin-left: 0; @@ -73,8 +71,6 @@ } .member-search-form { - @include new-style-dropdown; - position: relative; @media (min-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 2afb17334e3..e75a35d78ad 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -485,8 +485,6 @@ } .mr-source-target { - @include new-style-dropdown; - display: flex; flex-wrap: wrap; justify-content: space-between; @@ -608,8 +606,6 @@ } .mr-version-controls { - @include new-style-dropdown; - position: relative; background: $gray-light; color: $gl-text-color; @@ -727,7 +723,3 @@ font-size: 16px; } } - -.merge-request-form { - @include new-style-dropdown; -} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index ebb5d121433..6d4ccd53e12 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -23,8 +23,6 @@ .new-note, .note-edit-form { .note-form-actions { - @include new-style-dropdown; - position: relative; margin: $gl-padding 0 0; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 4d5613c292b..26e6e8688b6 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -490,8 +490,6 @@ ul.notes { } .note-actions { - @include new-style-dropdown; - align-self: flex-start; flex-shrink: 0; display: inline-flex; diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index c28b1e68008..bdf07a99daf 100644 --- a/app/assets/stylesheets/pages/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -14,7 +14,3 @@ font-size: 18px; } } - -.notification-form { - @include new-style-dropdown; -} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index cb24274c612..9805fc4f882 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -286,8 +286,6 @@ // Pipeline visualization .pipeline-actions { - @include new-style-dropdown; - border-bottom: 0; } @@ -703,9 +701,6 @@ button.mini-pipeline-graph-dropdown-toggle { } } -@include new-style-dropdown('.big-pipeline-graph-dropdown-menu'); -@include new-style-dropdown('.mini-pipeline-graph-dropdown-menu'); - // dropdown content for big and mini pipeline .big-pipeline-graph-dropdown-menu, .mini-pipeline-graph-dropdown-menu { @@ -804,7 +799,6 @@ button.mini-pipeline-graph-dropdown-toggle { font-weight: normal; line-height: $line-height-base; white-space: nowrap; - border-radius: 3px; .ci-job-name-component { align-items: center; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 674588752d2..6f4c678c4b8 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -323,8 +323,6 @@ } .project-repo-buttons { - @include new-style-dropdown; - .project-action-button .dropdown-menu { max-height: 250px; overflow-y: auto; @@ -898,8 +896,6 @@ pre.light-well { .new-protected-branch, .new-protected-tag { - @include new-style-dropdown; - label { margin-top: 6px; font-weight: $gl-font-weight-normal; @@ -919,8 +915,6 @@ pre.light-well { .protected-branches-list, .protected-tags-list { - @include new-style-dropdown; - margin-bottom: 30px; .settings-message { diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 402412eae71..6eb92c7baee 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -1,16 +1,3 @@ -.modal.popup-dialog { - display: block; - background-color: $black-transparent; - z-index: 2100; - - @media (min-width: $screen-md-min) { - .modal-dialog { - width: 600px; - margin: 30px auto; - } - } -} - .project-refs-form, .project-refs-target-form { display: inline-block; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index fe455a04960..49c8e546bf2 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -116,11 +116,6 @@ input[type="checkbox"]:hover { opacity: 0; display: block; left: -5px; - padding: 0; - - ul { - padding: 10px 0; - } } .dropdown-content { @@ -185,8 +180,6 @@ input[type="checkbox"]:hover { } .search-holder { - @include new-style-dropdown; - @media (min-width: $screen-sm-min) { display: -webkit-flex; display: flex; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 2139a029fc7..a79772ea37b 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -265,7 +265,3 @@ font-weight: $gl-font-weight-bold; } } - -.todos-filters { - @include new-style-dropdown; -} diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 5d14323e4bc..e0ee7e9aa3d 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,6 +1,4 @@ .tree-holder { - @include new-style-dropdown; - .nav-block { margin: 10px 0; diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 2ce26de1768..a94726887d9 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -1,4 +1,6 @@ class Admin::GroupsController < Admin::ApplicationController + include MembersPresentation + before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update] def index @@ -10,8 +12,10 @@ class Admin::GroupsController < Admin::ApplicationController def show @group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id]) - @members = @group.members.order("access_level DESC").page(params[:members_page]) - @requesters = AccessRequestsFinder.new(@group).execute(current_user) + @members = present_members( + @group.members.order("access_level DESC").page(params[:members_page])) + @requesters = present_members( + AccessRequestsFinder.new(@group).execute(current_user)) @projects = @group.projects.with_statistics.page(params[:projects_page]) end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 50cf2643390..3afe66c3566 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,4 +1,6 @@ class Admin::ProjectsController < Admin::ApplicationController + include MembersPresentation + before_action :project, only: [:show, :transfer, :repository_check] before_action :group, only: [:show, :transfer] @@ -19,11 +21,14 @@ class Admin::ProjectsController < Admin::ApplicationController def show if @group - @group_members = @group.members.order("access_level DESC").page(params[:group_members_page]) + @group_members = present_members( + @group.members.order("access_level DESC").page(params[:group_members_page])) end - @project_members = @project.members.page(params[:project_members_page]) - @requesters = AccessRequestsFinder.new(@project).execute(current_user) + @project_members = present_members( + @project.members.page(params[:project_members_page])) + @requesters = present_members( + AccessRequestsFinder.new(@project).execute(current_user)) end def transfer diff --git a/app/controllers/concerns/members_presentation.rb b/app/controllers/concerns/members_presentation.rb new file mode 100644 index 00000000000..c0622516fd3 --- /dev/null +++ b/app/controllers/concerns/members_presentation.rb @@ -0,0 +1,11 @@ +module MembersPresentation + extend ActiveSupport::Concern + + def present_members(members) + Gitlab::View::Presenter::Factory.new( + members, + current_user: current_user, + presenter_class: MembersPresenter + ).fabricate! + end +end diff --git a/app/controllers/concerns/with_performance_bar.rb b/app/controllers/concerns/with_performance_bar.rb index ed253042701..230bbe4b1aa 100644 --- a/app/controllers/concerns/with_performance_bar.rb +++ b/app/controllers/concerns/with_performance_bar.rb @@ -6,6 +6,7 @@ module WithPerformanceBar end def peek_enabled? + return true if Rails.env.development? return false unless Gitlab::PerformanceBar.enabled?(current_user) if RequestStore.active? diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 5919bf54468..21e77431176 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,5 +1,6 @@ class Groups::GroupMembersController < Groups::ApplicationController include MembershipActions + include MembersPresentation include SortingHelper # Authorize @@ -14,15 +15,17 @@ class Groups::GroupMembersController < Groups::ApplicationController @members = @members.search(params[:search]) if params[:search].present? @members = @members.sort(@sort) @members = @members.page(params[:page]).per(50) - @members.includes(:user) + @members = present_members(@members.includes(:user)) - @requesters = AccessRequestsFinder.new(@group).execute(current_user) + @requesters = present_members( + AccessRequestsFinder.new(@group).execute(current_user)) @group_member = @group.group_members.new end def update @group_member = @group.members_and_requesters.find(params[:id]) + .present(current_user: current_user) return render_403 unless can?(current_user, :update_group_member, @group_member) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 5a01a59481b..d7372beb9d3 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,5 +1,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController include MembershipActions + include MembersPresentation include SortingHelper # Authorize @@ -20,13 +21,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end - @project_members = @project_members.sort(@sort).page(params[:page]) - @requesters = AccessRequestsFinder.new(@project).execute(current_user) + @project_members = present_members(@project_members.sort(@sort).page(params[:page])) + @requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user)) @project_member = @project.project_members.new end def update @project_member = @project.members_and_requesters.find(params[:id]) + .present(current_user: current_user) return render_403 unless can?(current_user, :update_project_member, @project_member) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 212cdbb8157..0f110bd25c5 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -74,7 +74,7 @@ module IssuesHelper elsif item.try(:merged?) 'status-box-merged' elsif item.closed? - 'status-box-closed' + 'status-box-mr-closed' elsif item.try(:upcoming?) 'status-box-upcoming' else diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index e1ba7898ee6..c1c19062c91 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -1,6 +1,13 @@ module LabelsHelper include ActionView::Helpers::TagHelper + def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil) + return true if label.is_a?(GroupLabel) + return true unless project + + project.feature_available?(issuables_type, current_user) + end + # Link to a Label # # label - Label object to link to diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 41d471cc92f..a3129cac2b1 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -1,11 +1,4 @@ module MembersHelper - # Returns a `<action>_<source>_member` association, e.g.: - # - admin_project_member, update_project_member, destroy_project_member - # - admin_group_member, update_group_member, destroy_group_member - def action_member_permission(action, member) - "#{action}_#{member.type.underscore}".to_sym - end - def remove_member_message(member, user: nil) user = current_user if defined?(current_user) diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 77a82b895ce..50e17fe7717 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -5,7 +5,7 @@ module Emails @commit = @note.noteable @target_url = project_commit_url(*note_target_url_options) - mail_answer_thread(@commit, note_thread_options(recipient_id)) + mail_answer_note_thread(@commit, @note, note_thread_options(recipient_id)) end def note_issue_email(recipient_id, note_id) @@ -13,7 +13,7 @@ module Emails @issue = @note.noteable @target_url = project_issue_url(*note_target_url_options) - mail_answer_thread(@issue, note_thread_options(recipient_id)) + mail_answer_note_thread(@issue, @note, note_thread_options(recipient_id)) end def note_merge_request_email(recipient_id, note_id) @@ -21,7 +21,7 @@ module Emails @merge_request = @note.noteable @target_url = project_merge_request_url(*note_target_url_options) - mail_answer_thread(@merge_request, note_thread_options(recipient_id)) + mail_answer_note_thread(@merge_request, @note, note_thread_options(recipient_id)) end def note_snippet_email(recipient_id, note_id) @@ -29,7 +29,7 @@ module Emails @snippet = @note.noteable @target_url = project_snippet_url(*note_target_url_options) - mail_answer_thread(@snippet, note_thread_options(recipient_id)) + mail_answer_note_thread(@snippet, @note, note_thread_options(recipient_id)) end def note_personal_snippet_email(recipient_id, note_id) @@ -37,7 +37,7 @@ module Emails @snippet = @note.noteable @target_url = snippet_url(@note.noteable) - mail_answer_thread(@snippet, note_thread_options(recipient_id)) + mail_answer_note_thread(@snippet, @note, note_thread_options(recipient_id)) end private diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 9efabe3f44e..ec886e993c3 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -119,8 +119,8 @@ class Notify < BaseMailer headers['Reply-To'] = address fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze - headers['References'] ||= '' - headers['References'] << ' ' << fallback_reply_message_id + headers['References'] ||= [] + headers['References'] << fallback_reply_message_id @reply_by_email = true end @@ -156,6 +156,18 @@ class Notify < BaseMailer mail_thread(model, headers) end + def mail_answer_note_thread(model, note, headers = {}) + headers['Message-ID'] = message_id(note) + headers['In-Reply-To'] = message_id(note.references.last) + headers['References'] = note.references.map { |ref| message_id(ref) } + + headers['X-GitLab-Discussion-ID'] = note.discussion.id if note.part_of_discussion? + + headers[:subject]&.prepend('Re: ') + + mail_thread(model, headers) + end + def reply_key @reply_key ||= SentNotification.reply_key end diff --git a/app/models/blob.rb b/app/models/blob.rb index 29e762724e3..19ad110db58 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -77,9 +77,15 @@ class Blob < SimpleDelegator end def self.lazy(project, commit_id, path) - BatchLoader.for(commit_id: commit_id, path: path).batch do |items, loader| - project.repository.blobs_at(items.map(&:values)).each do |blob| - loader.call({ commit_id: blob.commit_id, path: blob.path }, blob) if blob + BatchLoader.for({ project: project, commit_id: commit_id, path: path }).batch do |items, loader| + items_by_project = items.group_by { |i| i[:project] } + + items_by_project.each do |project, items| + items = items.map { |i| i.values_at(:commit_id, :path) } + + project.repository.blobs_at(items).each do |blob| + loader.call({ project: blob.project, commit_id: blob.commit_id, path: blob.path }, blob) if blob + end end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 85960f1b6bb..83fe23606d1 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -491,7 +491,6 @@ module Ci end def valid_dependency? - return false unless complete? return false if artifacts_expired? return false if erased? diff --git a/app/models/commit.rb b/app/models/commit.rb index 307e4fcedfe..13c31111134 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -52,6 +52,20 @@ class Commit diffs.reduce(0) { |sum, d| sum + Gitlab::Git::Util.count_lines(d.diff) } end + def order_by(collection:, order_by:, sort:) + return collection unless %w[email name commits].include?(order_by) + return collection unless %w[asc desc].include?(sort) + + collection.sort do |a, b| + operands = [a, b].tap { |o| o.reverse! if sort == 'desc' } + + attr1, attr2 = operands.first.public_send(order_by), operands.second.public_send(order_by) # rubocop:disable PublicSend + + # use case insensitive comparison for string values + order_by.in?(%w[email name]) ? attr1.casecmp(attr2) : attr1 <=> attr2 + end + end + # Truncate sha to 8 characters def truncate_sha(sha) sha[0..MIN_SHA_LENGTH] diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 98776eab424..90ad644ce34 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -85,8 +85,7 @@ module CacheMarkdownField def cached_html_up_to_date?(markdown_field) html_field = cached_markdown_fields.html_field(markdown_field) - cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend - return false unless cached + return false if cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil? # rubocop:disable GitlabSecurity/PublicSend markdown_changed = attribute_changed?(markdown_field) || false html_changed = attribute_changed?(html_field) || false diff --git a/app/models/member.rb b/app/models/member.rb index 2fe5fda985f..c47145667b5 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -4,6 +4,7 @@ class Member < ActiveRecord::Base include Importable include Expirable include Gitlab::Access + include Presentable attr_accessor :raw_invite_token diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 422f138c4ea..c39789b047d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -8,6 +8,7 @@ class MergeRequest < ActiveRecord::Base include ManualInverseAssociation include EachBatch include ThrottledTouch + include Gitlab::Utils::StrongMemoize ignore_column :locked_at, :ref_fetched @@ -52,6 +53,7 @@ class MergeRequest < ActiveRecord::Base serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize after_create :ensure_merge_request_diff, unless: :importing? + after_update :clear_memoized_shas after_update :reload_diff_if_branch_changed # When this attribute is true some MR validation is ignored @@ -83,6 +85,14 @@ class MergeRequest < ActiveRecord::Base transition locked: :opened end + before_transition any => :opened do |merge_request| + merge_request.merge_jid = nil + + merge_request.run_after_commit do + UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) + end + end + state :opened state :closed state :merged @@ -387,13 +397,17 @@ class MergeRequest < ActiveRecord::Base end def source_branch_head - return unless source_project - - source_project.repository.commit(source_branch_ref) if source_branch_ref + strong_memoize(:source_branch_head) do + if source_project && source_branch_ref + source_project.repository.commit(source_branch_ref) + end + end end def target_branch_head - target_project.repository.commit(target_branch_ref) + strong_memoize(:target_branch_head) do + target_project.repository.commit(target_branch_ref) + end end def branch_merge_base_commit @@ -525,6 +539,13 @@ class MergeRequest < ActiveRecord::Base end end + def clear_memoized_shas + @target_branch_sha = @source_branch_sha = nil + + clear_memoization(:source_branch_head) + clear_memoization(:target_branch_head) + end + def reload_diff_if_branch_changed if (source_branch_changed? || target_branch_changed?) && (source_branch_head && target_branch_head) @@ -866,11 +887,11 @@ class MergeRequest < ActiveRecord::Base def state_icon_name if merged? - "check" + "git-merge" elsif closed? - "times" + "close" else - "circle-o" + "issue-open-m" end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index c37aa0a594b..e35de9b97ee 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -104,19 +104,19 @@ class MergeRequestDiff < ActiveRecord::Base def base_commit return unless base_commit_sha - project.commit(base_commit_sha) + project.commit_by(oid: base_commit_sha) end def start_commit return unless start_commit_sha - project.commit(start_commit_sha) + project.commit_by(oid: start_commit_sha) end def head_commit return unless head_commit_sha - project.commit(head_commit_sha) + project.commit_by(oid: head_commit_sha) end def commit_shas diff --git a/app/models/note.rb b/app/models/note.rb index c4c2ab8e67d..184fbd5f5ae 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -360,6 +360,16 @@ class Note < ActiveRecord::Base end end + def references + refs = [noteable] + + if part_of_discussion? + refs += discussion.notes.take_while { |n| n.id < id } + end + + refs + end + def expire_etag_cache return unless noteable&.discussions_rendered_on_frontend? @@ -401,6 +411,9 @@ class Note < ActiveRecord::Base end noteable_object&.touch + + # We return the noteable object so we can re-use it in EE for ElasticSearch. + noteable_object end def banzai_render_context(field) diff --git a/app/models/project.rb b/app/models/project.rb index 6ae15a0a50f..5183a216c53 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -659,7 +659,8 @@ class Project < ActiveRecord::Base end def import_started? - import? && import_status == 'started' + # import? does SQL work so only run it if it looks like there's an import running + import_status == 'started' && import? end def import_scheduled? @@ -1147,7 +1148,7 @@ class Project < ActiveRecord::Base def change_head(branch) if repository.branch_exists?(branch) repository.before_change_head - repository.write_ref('HEAD', "refs/heads/#{branch}") + repository.write_ref('HEAD', "refs/heads/#{branch}", force: true) repository.copy_gitattributes(branch) repository.after_change_head reload_default_branch diff --git a/app/models/repository.rb b/app/models/repository.rb index d1ae3304e4a..0c50d05bd96 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -19,6 +19,7 @@ class Repository attr_accessor :full_path, :disk_path, :project, :is_wiki delegate :ref_name_for_sha, to: :raw_repository + delegate :write_ref, to: :raw_repository CreateTreeError = Class.new(StandardError) @@ -237,11 +238,10 @@ class Repository # This will still fail if the file is corrupted (e.g. 0 bytes) begin - write_ref(keep_around_ref_name(sha), sha) - rescue Rugged::ReferenceError => ex - Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" - rescue Rugged::OSError => ex - raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/ + write_ref(keep_around_ref_name(sha), sha, force: true) + rescue Gitlab::Git::Repository::GitError => ex + # Necessary because https://gitlab.com/gitlab-org/gitlab-ce/issues/20156 + return true if ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/ Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" end @@ -251,10 +251,6 @@ class Repository ref_exists?(keep_around_ref_name(sha)) end - def write_ref(ref_path, sha) - rugged.references.create(ref_path, sha, force: true) - end - def diverging_commit_counts(branch) root_ref_hash = raw_repository.commit(root_ref).id cache.fetch(:"diverging_commit_counts_#{branch.name}") do @@ -690,7 +686,9 @@ class Repository def tags_sorted_by(value) case value - when 'name' + when 'name_asc' + VersionSorter.sort(tags) { |tag| tag.name } + when 'name_desc' VersionSorter.rsort(tags) { |tag| tag.name } when 'updated_desc' tags_sorted_by_committed_date.reverse @@ -701,10 +699,14 @@ class Repository end end - def contributors + # Params: + # + # order_by: name|email|commits + # sort: asc|desc default: 'asc' + def contributors(order_by: nil, sort: 'asc') commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true) - commits.group_by(&:author_email).map do |email, commits| + commits = commits.group_by(&:author_email).map do |email, commits| contributor = Gitlab::Contributor.new contributor.email = email @@ -718,6 +720,7 @@ class Repository contributor end + Commit.order_by(collection: commits, order_by: order_by, sort: sort) end def refs_contains_sha(ref_type, sha) @@ -994,7 +997,7 @@ class Repository end def create_ref(ref, ref_path) - raw_repository.write_ref(ref_path, ref) + write_ref(ref_path, ref) end def ls_files(ref) diff --git a/app/presenters/group_member_presenter.rb b/app/presenters/group_member_presenter.rb new file mode 100644 index 00000000000..8f53dfa105e --- /dev/null +++ b/app/presenters/group_member_presenter.rb @@ -0,0 +1,15 @@ +class GroupMemberPresenter < MemberPresenter + private + + def admin_member_permission + :admin_group_member + end + + def update_member_permission + :update_group_member + end + + def destroy_member_permission + :destroy_group_member + end +end diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb new file mode 100644 index 00000000000..7d2f9303b8f --- /dev/null +++ b/app/presenters/member_presenter.rb @@ -0,0 +1,38 @@ +class MemberPresenter < Gitlab::View::Presenter::Delegated + presents :member + + def access_level_roles + member.class.access_level_roles + end + + def can_resend_invite? + invite? && + can?(current_user, admin_member_permission, source) + end + + def can_update? + can?(current_user, update_member_permission, member) + end + + def can_remove? + can?(current_user, destroy_member_permission, member) + end + + def can_approve? + request? && can_update? + end + + private + + def admin_member_permission + raise NotImplementedError + end + + def update_member_permission + raise NotImplementedError + end + + def destroy_member_permission + raise NotImplementedError + end +end diff --git a/app/presenters/members_presenter.rb b/app/presenters/members_presenter.rb new file mode 100644 index 00000000000..e4aba37b69e --- /dev/null +++ b/app/presenters/members_presenter.rb @@ -0,0 +1,15 @@ +class MembersPresenter < Gitlab::View::Presenter::Delegated + include Enumerable + + presents :members + + def to_ary + to_a + end + + def each + members.each do |member| + yield member.present(current_user: current_user) + end + end +end diff --git a/app/presenters/project_member_presenter.rb b/app/presenters/project_member_presenter.rb new file mode 100644 index 00000000000..7f42d2b70df --- /dev/null +++ b/app/presenters/project_member_presenter.rb @@ -0,0 +1,15 @@ +class ProjectMemberPresenter < MemberPresenter + private + + def admin_member_permission + :admin_project_member + end + + def update_member_permission + :update_project_member + end + + def destroy_member_permission + :destroy_project_member + end +end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 85db2760e23..c8b112132b3 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -81,7 +81,7 @@ module Ci end def related_merge_requests - MergeRequest.where(source_project: pipeline.project, source_branch: pipeline.ref) + MergeRequest.opened.where(source_project: pipeline.project, source_branch: pipeline.ref) end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 2c51ac13815..e7463e6e25c 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -106,12 +106,14 @@ class IssuableBaseService < BaseService end def merge_quick_actions_into_params!(issuable) + original_description = params.fetch(:description, issuable.description) + description, command_params = QuickActions::InterpretService.new(project, current_user) - .execute(params[:description], issuable) + .execute(original_description, issuable) # Avoid a description already set on an issuable to be overwritten by a nil - params[:description] = description if params.key?(:description) + params[:description] = description if description params.merge!(command_params) end diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb index c13f289f61e..2a2bb0cae5b 100644 --- a/app/services/members/approve_access_request_service.rb +++ b/app/services/members/approve_access_request_service.rb @@ -35,8 +35,17 @@ module Members def can_update_access_requester?(access_requester, opts = {}) access_requester && ( opts[:force] || - can?(current_user, action_member_permission(:update, access_requester), access_requester) + can?(current_user, update_member_permission(access_requester), access_requester) ) end + + def update_member_permission(member) + case member + when GroupMember + :update_group_member + when ProjectMember + :update_project_member + end + end end end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 46c505baf8b..05b93ac8fdb 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -36,7 +36,16 @@ module Members end def can_destroy_member?(member) - member && can?(current_user, action_member_permission(:destroy, member), member) + member && can?(current_user, destroy_member_permission(member), member) + end + + def destroy_member_permission(member) + case member + when GroupMember + :destroy_group_member + when ProjectMember + :destroy_project_member + end end end end diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index a5686002328..20ca6ec969a 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -83,12 +83,12 @@ You're all done! - elsif current_user.todos.any? .todos-all-done - .svg-content + .svg-content.svg-250 = image_tag 'illustrations/todos_all_done.svg' - if todos_filter_empty? %h4.text-center = Gitlab.config.gitlab.no_todos_messages.sample - %p.text-center + %p Are you looking for things to do? Take a look at = succeed "," do = link_to "the opened issues", issues_dashboard_path @@ -104,7 +104,7 @@ = image_tag 'illustrations/todos_empty.svg' .todos-empty-content %h4 - Todos let you see what you should do next. + Todos let you see what you should do next %p When an issue or merge request is assigned to you, or when you %strong diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 25ed610466a..eba9cd253bb 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,7 +1,7 @@ -.page-with-sidebar{ class: page_with_sidebar_class } +.layout-page{ class: page_with_sidebar_class } - if defined?(nav) && nav = render "layouts/nav/sidebar/#{nav}" - .content-wrapper.page-with-new-nav + .content-wrapper = render 'shared/outdated_browser' .mobile-overlay .alert-wrapper diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml index a7370180bf6..32a24c101fc 100644 --- a/app/views/layouts/nav/projects_dropdown/_show.html.haml +++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml @@ -1,4 +1,4 @@ -- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: @project.web_url, avatar_url: @project.avatar_url } if @project&.persisted? +- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted? .projects-dropdown-container .project-dropdown-sidebar %ul diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml index 2a178325041..5b092427496 100644 --- a/app/views/projects/blob/_template_selectors.html.haml +++ b/app/views/projects/blob/_template_selectors.html.haml @@ -3,15 +3,15 @@ Template .template-selector-dropdowns-wrap .template-type-selector.js-template-type-selector-wrap.hidden - = dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } ) + = dropdown_tag("Choose type", options: { toggle_class: 'js-template-type-selector', title: "Choose a template type" } ) .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a license template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) + = dropdown_tag("Apply a license template", options: { toggle_class: 'js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) + = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) + = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) + = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) .template-selectors-undo-menu.hidden %span.text-info Template applied %button.btn.btn-sm.btn-info Undo diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 2baaaf6ac5b..e9d8fc75142 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -20,7 +20,7 @@ .col-sm-10.create-from .dropdown = hidden_field_tag :ref, default_ref - = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do + = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do .text-left.dropdown-toggle-text= default_ref = icon('chevron-down') = render 'shared/ref_dropdown', dropdown_class: 'wide' diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml index e629cc58b06..b525f4efc83 100644 --- a/app/views/projects/clusters/_empty_state.html.haml +++ b/app/views/projects/clusters/_empty_state.html.haml @@ -1,12 +1,12 @@ .row.empty-state .col-xs-12 .svg-content= image_tag 'illustrations/clusters_empty.svg' - .col-xs-12.text-center + .col-xs-12 .text-content - %h4= s_('ClusterIntegration|Integrate cluster automation') + %h4.text-center= s_('ClusterIntegration|Integrate cluster automation') - link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') %p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page} - %p + .text-center = link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success' diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 618a6355d23..d66066a6d0b 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -38,8 +38,8 @@ .commiter - commit_author_link = commit_author_link(commit, avatar: false, size: 24) - - commit_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom') - - commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } + - commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom') + - commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } #{ commit_text.html_safe } .commit-actions.flex-row.hidden-xs diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 56cf16c3778..ad94113fffd 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -15,9 +15,9 @@ #prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'), "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'), - "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started'), - "empty-loading-svg-path": image_path('illustrations/monitoring/loading'), - "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect'), + "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'), + "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'), + "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'), "additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json), "project-path": project_path(@project), "tags-path": project_tags_path(@project), diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index d260aaee2d3..eab7879c7bf 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -14,12 +14,12 @@ .detail-page-header .detail-page-header-body - .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } - = icon('check', class: "hidden-sm hidden-md hidden-lg") + .issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) } + = sprite_icon('mobile-issue-close', size: 16, css_class: 'hidden-sm hidden-md hidden-lg') %span.hidden-xs Closed .issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) } - = icon('circle-o', class: "hidden-sm hidden-md hidden-lg") + = sprite_icon('issue-open-m', size: 16, css_class: 'hidden-sm hidden-md hidden-lg') %span.hidden-xs Open .issuable-meta diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 9df14265737..22c8b6b513d 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -7,7 +7,7 @@ .detail-page-header .detail-page-header-body .issuable-status-box.status-box{ class: status_box_class(@merge_request) } - = icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg") + = sprite_icon(@merge_request.state_icon_name, size: 16, css_class: 'hidden-sm hidden-md hidden-lg') %span.hidden-xs = @merge_request.state_human_name @@ -27,7 +27,7 @@ .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul - if can_update_merge_request - %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'js-issuable-edit' + %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - unless current_user == @merge_request.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)) - if can_update_merge_request diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index e71d58ec26d..16bcf671c25 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,11 +1,13 @@ +- project = local_assigns.fetch(:project) +- members = local_assigns.fetch(:members) + .panel.panel-default .panel-heading.flex-project-members-panel %span.flex-project-title Members of - %strong - #{@project.name} - %span.badge= @project_members.total_count - = form_tag project_project_members_path(@project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do + %strong= project.name + %span.badge= members.total_count + = form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index fd5d3ec56da..d81103c3a92 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -37,5 +37,5 @@ - if @group_links.any? = render 'projects/project_members/groups', group_links: @group_links - = render 'projects/project_members/team', members: @project_members + = render 'projects/project_members/team', project: @project, members: @project_members = paginate @project_members, theme: "gitlab" diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 031efa903c5..6e105a5521a 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -14,13 +14,13 @@ .form-group = label_tag :tag_name, nil, class: 'control-label' .col-sm-10 - = text_field_tag :tag_name, params[:tag_name], required: true, tabindex: 1, autofocus: true, class: 'form-control' + = text_field_tag :tag_name, params[:tag_name], required: true, autofocus: true, class: 'form-control' .form-group = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10.create-from .dropdown = hidden_field_tag :ref, default_ref - = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do + = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do .text-left.dropdown-toggle-text= default_ref = render 'shared/ref_dropdown', dropdown_class: 'wide' .help-block @@ -28,7 +28,7 @@ .form-group = label_tag :message, nil, class: 'control-label' .col-sm-10 - = text_area_tag :message, @message, required: false, tabindex: 3, class: 'form-control', rows: 5 + = text_area_tag :message, @message, required: false, class: 'form-control', rows: 5 .help-block = s_('TagsPage|Optionally, add a message to the tag.') %hr @@ -41,6 +41,6 @@ .help-block = s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.') .form-actions - = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create', tabindex: 3 + = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create' = link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel' %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 3fcc33044e9..81d07074325 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -2,6 +2,8 @@ - status = label_subscription_status(label, @project).inquiry if current_user - subject = local_assigns[:subject] - toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user +- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) +- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) %li{ id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label @@ -12,12 +14,14 @@ = icon('caret-down') .dropdown-menu.dropdown-menu-align-right %ul - %li - = link_to_label(label, subject: subject, type: :merge_request) do - View merge requests - %li - = link_to_label(label, subject: subject) do - View open issues + - if show_label_merge_requests_link + %li + = link_to_label(label, subject: subject, type: :merge_request) do + View merge requests + - if show_label_issues_link + %li + = link_to_label(label, subject: subject) do + View open issues - if current_user %li.label-subscription - if can_subscribe_to_label_in_different_levels?(label) @@ -35,13 +39,20 @@ %li = link_to 'Edit', edit_label_path(label) %li - = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, data: {confirm: 'Remove this label? Are you sure?'} + = link_to 'Delete', + destroy_label_path(label), + title: 'Delete', + method: :delete, + data: {confirm: 'Remove this label? Are you sure?'}, + class: 'text-danger' .pull-right.hidden-xs.hidden-sm.hidden-md - = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do - view merge requests - = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action btn-link') do - view open issues + - if show_label_merge_requests_link + = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do + view merge requests + - if show_label_issues_link + = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action btn-link') do + view open issues - if current_user .label-subscription.inline diff --git a/app/views/shared/_show_aside.html.haml b/app/views/shared/_show_aside.html.haml deleted file mode 100644 index 3ac9b11b4fa..00000000000 --- a/app/views/shared/_show_aside.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= link_to '#aside', class: 'show-aside' do - %i.fa.fa-angle-left diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index de26fa8bbf3..e039a73cd3b 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -6,18 +6,21 @@ .col-xs-12 .svg-content = image_tag 'illustrations/issues.svg' - .col-xs-12.text-center + .col-xs-12 .text-content - if has_button && current_user %h4 - The Issue Tracker is the place to add things that need to be improved or solved in a project + = _("The Issue Tracker is the place to add things that need to be improved or solved in a project") %p - Issues can be bugs, tasks or ideas to be discussed. - Also, issues are searchable and filterable. - - if project_select_button - = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues - - else - = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link' + = _("Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.") + .text-center + - if project_select_button + = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues + - else + = link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link' - else + %h4.text-center= _("There are no issues to show") + %p + = _("The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.") .text-center - %h4 There are no issues to show. + = link_to _('Register / Sign In'), new_user_session_path, class: 'btn btn-success' diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml index a65634dce53..04db9de3606 100644 --- a/app/views/shared/empty_states/_labels.html.haml +++ b/app/views/shared/empty_states/_labels.html.haml @@ -2,10 +2,10 @@ .col-xs-12 .svg-content = image_tag 'illustrations/labels.svg' - .col-xs-12.text-center + .col-xs-12 .text-content - %h4 Labels can be applied to issues and merge requests to categorize them. - %p You can also star a label to make it a priority label. + %h4= _("Labels can be applied to issues and merge requests to categorize them.") + %p= _("You can also star a label to make it a priority label.") - if can?(current_user, :admin_label, @project) - = link_to 'New label', new_project_label_path(@project), class: 'btn btn-new', title: 'New label', id: 'new_label_link' - = link_to 'Generate a default set of labels', generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: 'Generate a default set of labels', id: 'generate_labels_link' + = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-new', title: _('New label'), id: 'new_label_link' + = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link' diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml index 67f906903e9..2edf3557df4 100644 --- a/app/views/shared/empty_states/_merge_requests.html.haml +++ b/app/views/shared/empty_states/_merge_requests.html.haml @@ -6,17 +6,18 @@ .col-xs-12 .svg-content = image_tag 'illustrations/merge_requests.svg' - .col-xs-12.text-center + .col-xs-12 .text-content - if has_button %h4 - Merge requests are a place to propose changes you've made to a project and discuss those changes with others. + = _("Merge requests are a place to propose changes you've made to a project and discuss those changes with others") %p - Interested parties can even contribute by pushing commits if they want to. - - if project_select_button - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: 'New merge request', type: :merge_requests - - else - = link_to 'New merge request', button_path, class: 'btn btn-new', title: 'New merge request', id: 'new_merge_request_link' + = _("Interested parties can even contribute by pushing commits if they want to.") + .text-center + - if project_select_button + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests + - else + = link_to _('New merge request'), button_path, class: 'btn btn-new', title: _('New merge request'), id: 'new_merge_request_link' - else %h4.text-center - There are no merge requests to show. + = _("There are no merge requests to show") diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 2c27dd638a7..71878e93255 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -1,9 +1,9 @@ - show_roles = local_assigns.fetch(:show_roles, true) - show_controls = local_assigns.fetch(:show_controls, true) - force_mobile_view = local_assigns.fetch(:force_mobile_view, false) +- member = local_assigns.fetch(:member) - user = local_assigns.fetch(:user, member.user) - source = member.source -- can_admin_member = can?(current_user, action_member_permission(:update, member), member) %li.member{ class: dom_class(member), id: dom_id(member) } %span.list-item-name @@ -50,18 +50,17 @@ .controls.member-controls - if show_controls && member.source == current_resource - - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source) + - if member.can_resend_invite? = link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]), method: :post, class: 'btn btn-default prepend-left-10 hidden-xs', title: 'Resend invite' - - if user != current_user && can_admin_member + - if user != current_user && member.can_update? = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.hidden_field :access_level .member-form-control.dropdown.append-right-5 %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", - disabled: !can_admin_member, data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } } %span.dropdown-toggle-text = member.human_access @@ -70,23 +69,22 @@ = dropdown_title("Change permissions") .dropdown-content %ul - - member.class.access_level_roles.each do |role, role_id| + - member.access_level_roles.each do |role, role_id| %li = link_to role, "javascript:void(0)", class: ("is-active" if member.access_level == role_id), data: { id: role_id, el_id: dom_id(member) } .prepend-left-5.clearable-input.member-form-control - = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) } + = f.text_field :expires_at, + class: 'form-control js-access-expiration-date js-member-update-control', + placeholder: 'Expiration date', + id: "member_expires_at_#{member.id}", + data: { el_id: dom_id(member) } %i.clear-icon.js-clear-input - else %span.member-access-text= member.human_access - - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source) - = link_to 'Resend invite', polymorphic_path([:resend_invite, member]), - method: :post, - class: 'btn btn-default prepend-left-10 visible-xs-block' - - - elsif member.request? && can_admin_member + - if member.can_approve? = link_to polymorphic_path([:approve_access_request, member]), method: :post, class: 'btn btn-success prepend-left-10', @@ -96,7 +94,7 @@ - unless force_mobile_view = icon('check inverse', class: 'hidden-xs') - - if can?(current_user, action_member_permission(:destroy, member), member) + - if member.can_remove? - if current_user == user = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]), method: :delete, diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml index 09b9944082f..1fbd6bcc4cb 100644 --- a/app/views/shared/members/_requests.html.haml +++ b/app/views/shared/members/_requests.html.haml @@ -1,10 +1,13 @@ +- membership_source = local_assigns.fetch(:membership_source) +- requesters = local_assigns.fetch(:requesters) - force_mobile_view = local_assigns.fetch(:force_mobile_view, false) -- if requesters.any? - .panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) } - .panel-heading - Users requesting access to - %strong= membership_source.name - %span.badge= requesters.size - %ul.content-list.members-list - = render partial: 'shared/members/member', collection: requesters, as: :member, locals: { force_mobile_view: force_mobile_view } +- return if requesters.empty? + +.panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) } + .panel-heading + Users requesting access to + %strong= membership_source.name + %span.badge= requesters.size + %ul.content-list.members-list + = render partial: 'shared/members/member', collection: requesters, as: :member, locals: { force_mobile_view: force_mobile_view } diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml new file mode 100644 index 00000000000..ba31a5aa9c2 --- /dev/null +++ b/app/workers/all_queues.yml @@ -0,0 +1,98 @@ +--- +- cronjob:admin_email +- cronjob:expire_build_artifacts +- cronjob:gitlab_usage_ping +- cronjob:import_export_project_cleanup +- cronjob:pipeline_schedule +- cronjob:prune_old_events +- cronjob:remove_expired_group_links +- cronjob:remove_expired_members +- cronjob:remove_old_web_hook_logs +- cronjob:remove_unreferenced_lfs_objects +- cronjob:repository_archive_cache +- cronjob:repository_check_batch +- cronjob:requests_profiles +- cronjob:schedule_update_user_activity +- cronjob:stuck_ci_jobs +- cronjob:stuck_import_jobs +- cronjob:stuck_merge_jobs +- cronjob:trending_projects + +- gcp_cluster:cluster_install_app +- gcp_cluster:cluster_provision +- gcp_cluster:cluster_wait_for_app_installation +- gcp_cluster:wait_for_cluster_creation + +- github_import_advance_stage +- github_importer:github_import_import_diff_note +- github_importer:github_import_import_issue +- github_importer:github_import_import_note +- github_importer:github_import_import_pull_request +- github_importer:github_import_refresh_import_jid +- github_importer:github_import_stage_finish_import +- github_importer:github_import_stage_import_base_data +- github_importer:github_import_stage_import_issues_and_diff_notes +- github_importer:github_import_stage_import_notes +- github_importer:github_import_stage_import_pull_requests +- github_importer:github_import_stage_import_repository + +- pipeline_cache:expire_job_cache +- pipeline_cache:expire_pipeline_cache +- pipeline_creation:create_pipeline +- pipeline_default:build_coverage +- pipeline_default:build_trace_sections +- pipeline_default:pipeline_metrics +- pipeline_default:pipeline_notification +- pipeline_default:update_head_pipeline_for_merge_request +- pipeline_hooks:build_hooks +- pipeline_hooks:pipeline_hooks +- pipeline_processing:build_finished +- pipeline_processing:build_queue +- pipeline_processing:build_success +- pipeline_processing:pipeline_process +- pipeline_processing:pipeline_success +- pipeline_processing:pipeline_update +- pipeline_processing:stage_update + +- repository_check:repository_check_clear +- repository_check:repository_check_single_repository + +- default +- mailers # ActionMailer::DeliveryJob.queue_name + +- authorized_projects +- background_migration +- create_gpg_signature +- delete_merged_branches +- delete_user +- email_receiver +- emails_on_push +- expire_build_instance_artifacts +- git_garbage_collect +- gitlab_shell +- group_destroy +- invalid_gpg_signature_update +- irker +- merge +- namespaceless_project_destroy +- new_issue +- new_merge_request +- new_note +- pages +- post_receive +- process_commit +- project_cache +- project_destroy +- project_export +- project_migrate_hashed_storage +- project_service +- propagate_service_template +- reactive_caching +- repository_fork +- repository_import +- storage_migrator +- system_hook_push +- update_merge_requests +- update_user_activity +- upload_checksum +- web_hook diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index 5efa9180f5e..97d80305bec 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -2,7 +2,7 @@ class BuildFinishedWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb index 6705a1c2709..cbfca8c342c 100644 --- a/app/workers/build_hooks_worker.rb +++ b/app/workers/build_hooks_worker.rb @@ -2,7 +2,7 @@ class BuildHooksWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :hooks + queue_namespace :pipeline_hooks def perform(build_id) Ci::Build.find_by(id: build_id) diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb index fc775a84dc0..e4f4e6c1d9e 100644 --- a/app/workers/build_queue_worker.rb +++ b/app/workers/build_queue_worker.rb @@ -2,7 +2,7 @@ class BuildQueueWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index ec049821ad7..4b9097bc5e4 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -2,7 +2,7 @@ class BuildSuccessWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb index 9c3bdabc49e..37586e161c9 100644 --- a/app/workers/concerns/application_worker.rb +++ b/app/workers/concerns/application_worker.rb @@ -3,13 +3,23 @@ Sidekiq::Worker.extend ActiveSupport::Concern module ApplicationWorker extend ActiveSupport::Concern - include Sidekiq::Worker + include Sidekiq::Worker # rubocop:disable Cop/IncludeSidekiqWorker included do - sidekiq_options queue: base_queue_name + set_queue end module ClassMethods + def inherited(subclass) + subclass.set_queue + end + + def set_queue + queue_name = [queue_namespace, base_queue_name].compact.join(':') + + sidekiq_options queue: queue_name # rubocop:disable Cop/SidekiqOptionsQueue + end + def base_queue_name name .sub(/\AGitlab::/, '') @@ -18,6 +28,16 @@ module ApplicationWorker .tr('/', '_') end + def queue_namespace(new_namespace = nil) + if new_namespace + sidekiq_options queue_namespace: new_namespace + + set_queue + else + get_sidekiq_options['queue_namespace']&.to_s + end + end + def queue get_sidekiq_options['queue'].to_s end diff --git a/app/workers/concerns/cluster_queue.rb b/app/workers/concerns/cluster_queue.rb index a5074d13220..24b9f145220 100644 --- a/app/workers/concerns/cluster_queue.rb +++ b/app/workers/concerns/cluster_queue.rb @@ -5,6 +5,6 @@ module ClusterQueue extend ActiveSupport::Concern included do - sidekiq_options queue: :gcp_cluster + queue_namespace :gcp_cluster end end diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb index e918bb011e0..b6581779f6a 100644 --- a/app/workers/concerns/cronjob_queue.rb +++ b/app/workers/concerns/cronjob_queue.rb @@ -4,6 +4,7 @@ module CronjobQueue extend ActiveSupport::Concern included do - sidekiq_options queue: :cronjob, retry: false + queue_namespace :cronjob + sidekiq_options retry: false end end diff --git a/app/workers/concerns/gitlab/github_import/queue.rb b/app/workers/concerns/gitlab/github_import/queue.rb index a2bee361b86..22c2ce458e8 100644 --- a/app/workers/concerns/gitlab/github_import/queue.rb +++ b/app/workers/concerns/gitlab/github_import/queue.rb @@ -4,12 +4,14 @@ module Gitlab extend ActiveSupport::Concern included do + queue_namespace :github_importer + # If a job produces an error it may block a stage from advancing # forever. To prevent this from happening we prevent jobs from going to # the dead queue. This does mean some resources may not be imported, but # this is better than a project being stuck in the "import" state # forever. - sidekiq_options queue: 'github_importer', dead: false, retry: 5 + sidekiq_options dead: false, retry: 5 end end end diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb index ddf45b91345..e77093a6902 100644 --- a/app/workers/concerns/pipeline_queue.rb +++ b/app/workers/concerns/pipeline_queue.rb @@ -5,14 +5,6 @@ module PipelineQueue extend ActiveSupport::Concern included do - sidekiq_options queue: 'pipeline_default' - end - - class_methods do - def enqueue_in(group:) - raise ArgumentError, 'Unspecified queue group!' if group.empty? - - sidekiq_options queue: "pipeline_#{group}" - end + queue_namespace :pipeline_default end end diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb index a597321ccf4..43fb66c31b0 100644 --- a/app/workers/concerns/repository_check_queue.rb +++ b/app/workers/concerns/repository_check_queue.rb @@ -3,6 +3,8 @@ module RepositoryCheckQueue extend ActiveSupport::Concern included do - sidekiq_options queue: :repository_check, retry: false + queue_namespace :repository_check + + sidekiq_options retry: false end end diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb index 00cd7b85b9f..c3ac35e54f5 100644 --- a/app/workers/create_pipeline_worker.rb +++ b/app/workers/create_pipeline_worker.rb @@ -2,7 +2,7 @@ class CreatePipelineWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :creation + queue_namespace :pipeline_creation def perform(project_id, user_id, ref, source, params = {}) project = Project.find(project_id) diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb index a591e2da519..7217364a9f2 100644 --- a/app/workers/expire_job_cache_worker.rb +++ b/app/workers/expire_job_cache_worker.rb @@ -2,7 +2,7 @@ class ExpireJobCacheWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :cache + queue_namespace :pipeline_cache def perform(job_id) job = CommitStatus.joins(:pipeline, :project).find_by(id: job_id) diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb index a3ac32b437d..3e34de22c19 100644 --- a/app/workers/expire_pipeline_cache_worker.rb +++ b/app/workers/expire_pipeline_cache_worker.rb @@ -2,7 +2,7 @@ class ExpirePipelineCacheWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :cache + queue_namespace :pipeline_cache def perform(pipeline_id) pipeline = Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb index 400396d5755..f7f498af840 100644 --- a/app/workers/gitlab/github_import/advance_stage_worker.rb +++ b/app/workers/gitlab/github_import/advance_stage_worker.rb @@ -9,7 +9,7 @@ module Gitlab class AdvanceStageWorker include ApplicationWorker - sidekiq_options queue: 'github_importer_advance_stage', dead: false + sidekiq_options dead: false INTERVAL = 30.seconds.to_i diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 62f733c02fc..3ec81d040b4 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -1,7 +1,7 @@ class PagesWorker include ApplicationWorker - sidekiq_options queue: :pages, retry: false + sidekiq_options retry: false def perform(action, *arg) send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb index 661c29efe88..c94918ff4ee 100644 --- a/app/workers/pipeline_hooks_worker.rb +++ b/app/workers/pipeline_hooks_worker.rb @@ -2,7 +2,7 @@ class PipelineHooksWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :hooks + queue_namespace :pipeline_hooks def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb index 07dbf6a971e..24424b3f472 100644 --- a/app/workers/pipeline_process_worker.rb +++ b/app/workers/pipeline_process_worker.rb @@ -2,7 +2,7 @@ class PipelineProcessWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb index 68c40a259e1..2ab0739a17f 100644 --- a/app/workers/pipeline_success_worker.rb +++ b/app/workers/pipeline_success_worker.rb @@ -2,7 +2,7 @@ class PipelineSuccessWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb index 24a8a9fbed5..fc9da2d45b1 100644 --- a/app/workers/pipeline_update_worker.rb +++ b/app/workers/pipeline_update_worker.rb @@ -2,7 +2,7 @@ class PipelineUpdateWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb index 69f2318d83b..e4b683fca33 100644 --- a/app/workers/stage_update_worker.rb +++ b/app/workers/stage_update_worker.rb @@ -2,7 +2,7 @@ class StageUpdateWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(stage_id) Ci::Stage.find_by(id: stage_id).try do |stage| diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb index 36d2a2e6466..16394293c79 100644 --- a/app/workers/stuck_merge_jobs_worker.rb +++ b/app/workers/stuck_merge_jobs_worker.rb @@ -23,7 +23,12 @@ class StuckMergeJobsWorker merge_requests = MergeRequest.where(id: completed_ids) merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged) - merge_requests.where(merge_commit_sha: nil).update_all(state: :opened, merge_jid: nil) + + merge_requests_to_reopen = merge_requests.where(merge_commit_sha: nil) + + # Do not reopen merge requests using direct queries. + # We rely on state machine callbacks to update head_pipeline_id + merge_requests_to_reopen.each(&:unlock_mr) Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}") end diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb index 0a2e9b63578..f09d89aa170 100644 --- a/app/workers/update_head_pipeline_for_merge_request_worker.rb +++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb @@ -1,15 +1,25 @@ class UpdateHeadPipelineForMergeRequestWorker include ApplicationWorker - - sidekiq_options queue: 'pipeline_default' + include PipelineQueue def perform(merge_request_id) merge_request = MergeRequest.find(merge_request_id) pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last return unless pipeline && pipeline.latest? - raise ArgumentError, 'merge request sha does not equal pipeline sha' if merge_request.diff_head_sha != pipeline.sha + + if merge_request.diff_head_sha != pipeline.sha + log_error_message_for(merge_request) + + return + end merge_request.update_attribute(:head_pipeline_id, pipeline.id) end + + def log_error_message_for(merge_request) + Rails.logger.error( + "Outdated head pipeline for active merge request: id=#{merge_request.id}, source_branch=#{merge_request.source_branch}, diff_head_sha=#{merge_request.diff_head_sha}" + ) + end end diff --git a/changelogs/unreleased/13695-order-contributors-in-api.yml b/changelogs/unreleased/13695-order-contributors-in-api.yml new file mode 100644 index 00000000000..26bf8650a4a --- /dev/null +++ b/changelogs/unreleased/13695-order-contributors-in-api.yml @@ -0,0 +1,5 @@ +--- +title: Adds ordering to projects contributors in API +merge_request: 15469 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/25317-prioritize-author-date-over-commit.yml b/changelogs/unreleased/25317-prioritize-author-date-over-commit.yml new file mode 100644 index 00000000000..a5f6d316a7d --- /dev/null +++ b/changelogs/unreleased/25317-prioritize-author-date-over-commit.yml @@ -0,0 +1,5 @@ +--- +title: Show authored date rather than committed date on the commit list +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/28004-consider-refactoring-member-view-by-using-presenter.yml b/changelogs/unreleased/28004-consider-refactoring-member-view-by-using-presenter.yml new file mode 100644 index 00000000000..0e91d4ae403 --- /dev/null +++ b/changelogs/unreleased/28004-consider-refactoring-member-view-by-using-presenter.yml @@ -0,0 +1,4 @@ +--- +title: Refactor member view using a Presenter +merge_request: 9645 +author: TM Lee diff --git a/changelogs/unreleased/33926-update-issuable-icons.yml b/changelogs/unreleased/33926-update-issuable-icons.yml new file mode 100644 index 00000000000..87076dde545 --- /dev/null +++ b/changelogs/unreleased/33926-update-issuable-icons.yml @@ -0,0 +1,5 @@ +--- +title: Update issuable status icons +merge_request: 15898 +author: +type: changed diff --git a/changelogs/unreleased/40285-prometheus-loading-screen-no-longer-seems-to-appear.yml b/changelogs/unreleased/40285-prometheus-loading-screen-no-longer-seems-to-appear.yml new file mode 100644 index 00000000000..978930a5b8c --- /dev/null +++ b/changelogs/unreleased/40285-prometheus-loading-screen-no-longer-seems-to-appear.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken illustration images for monitoring page empty states +merge_request: 15889 +author: +type: fixed diff --git a/changelogs/unreleased/40509_sorting_tags_api.yml b/changelogs/unreleased/40509_sorting_tags_api.yml new file mode 100644 index 00000000000..38b198d0fe3 --- /dev/null +++ b/changelogs/unreleased/40509_sorting_tags_api.yml @@ -0,0 +1,5 @@ +--- +title: add support for sorting in tags api +merge_request: 15772 +author: haseebeqx +type: added diff --git a/changelogs/unreleased/40895-fix-frequent-projects-stale-path.yml b/changelogs/unreleased/40895-fix-frequent-projects-stale-path.yml new file mode 100644 index 00000000000..485133b46a7 --- /dev/null +++ b/changelogs/unreleased/40895-fix-frequent-projects-stale-path.yml @@ -0,0 +1,5 @@ +--- +title: Use relative URL for projects to avoid storing domains +merge_request: 15876 +author: +type: fixed diff --git a/changelogs/unreleased/add-tcp-check-rake-task.yml b/changelogs/unreleased/add-tcp-check-rake-task.yml new file mode 100644 index 00000000000..a7c04bd0d55 --- /dev/null +++ b/changelogs/unreleased/add-tcp-check-rake-task.yml @@ -0,0 +1,5 @@ +--- +title: Add a gitlab:tcp_check rake task +merge_request: 15759 +author: +type: added diff --git a/changelogs/unreleased/fix-create-mr-from-issue-with-template.yml b/changelogs/unreleased/fix-create-mr-from-issue-with-template.yml new file mode 100644 index 00000000000..8668aa18669 --- /dev/null +++ b/changelogs/unreleased/fix-create-mr-from-issue-with-template.yml @@ -0,0 +1,5 @@ +--- +title: Execute quick actions (if present) when creating MR from issue +merge_request: 15810 +author: +type: fixed diff --git a/changelogs/unreleased/issue-description-field-typo.yml b/changelogs/unreleased/issue-description-field-typo.yml new file mode 100644 index 00000000000..9c4c179876d --- /dev/null +++ b/changelogs/unreleased/issue-description-field-typo.yml @@ -0,0 +1,5 @@ +--- +title: Fixed typo for issue description field declaration +merge_request: +author: Marcus Amargi +type: fixed diff --git a/changelogs/unreleased/optimize-issues-avoid-noop-empty-cache-updates2.yml b/changelogs/unreleased/optimize-issues-avoid-noop-empty-cache-updates2.yml new file mode 100644 index 00000000000..e0c3136be69 --- /dev/null +++ b/changelogs/unreleased/optimize-issues-avoid-noop-empty-cache-updates2.yml @@ -0,0 +1,6 @@ +--- +title: Treat empty markdown and html strings as valid cached text, not missing cache + that needs to be updated +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/optimize-projects-for-imported-projects.yml b/changelogs/unreleased/optimize-projects-for-imported-projects.yml new file mode 100644 index 00000000000..13186fa36d5 --- /dev/null +++ b/changelogs/unreleased/optimize-projects-for-imported-projects.yml @@ -0,0 +1,6 @@ +--- +title: check the import_status field before doing SQL operations to check the import + url +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/remove-tabindexes-from-tag-form.yml b/changelogs/unreleased/remove-tabindexes-from-tag-form.yml new file mode 100644 index 00000000000..a15bf2a7a4f --- /dev/null +++ b/changelogs/unreleased/remove-tabindexes-from-tag-form.yml @@ -0,0 +1,5 @@ +--- +title: removed tabindexes from tag form +merge_request: +author: Marcus Amargi +type: changed diff --git a/changelogs/unreleased/sophie-h-gitlab-ce-patch-15.yml b/changelogs/unreleased/sophie-h-gitlab-ce-patch-15.yml new file mode 100644 index 00000000000..b5e3210c737 --- /dev/null +++ b/changelogs/unreleased/sophie-h-gitlab-ce-patch-15.yml @@ -0,0 +1,5 @@ +--- +title: Hide link to issues/MRs from labels list if issues/MRs are disabled. +merge_request: 15863 +author: Sophie Herold +type: fixed diff --git a/changelogs/unreleased/tc-correct-email-in-reply-to.yml b/changelogs/unreleased/tc-correct-email-in-reply-to.yml new file mode 100644 index 00000000000..1c8043f6a5c --- /dev/null +++ b/changelogs/unreleased/tc-correct-email-in-reply-to.yml @@ -0,0 +1,5 @@ +--- +title: Make mail notifications of discussion notes In-Reply-To of each other +merge_request: 14289 +author: +type: changed diff --git a/changelogs/unreleased/zj-memoization-mr-commits.yml b/changelogs/unreleased/zj-memoization-mr-commits.yml new file mode 100644 index 00000000000..59dfc6d6049 --- /dev/null +++ b/changelogs/unreleased/zj-memoization-mr-commits.yml @@ -0,0 +1,5 @@ +--- +title: Cache commits for MergeRequest diffs +merge_request: +author: +type: performance diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index bfab8c77a4b..cc9167d29b9 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -1,8 +1,22 @@ -require 'flipper/middleware/memoizer' +require 'flipper/adapters/active_record' +require 'flipper/adapters/active_support_cache_store' -unless Rails.env.test? - Rails.application.config.middleware.use Flipper::Middleware::Memoizer, - lambda { Feature.flipper } +Flipper.configure do |config| + config.default do + adapter = Flipper::Adapters::ActiveRecord.new( + feature_class: Feature::FlipperFeature, gate_class: Feature::FlipperGate) + cached_adapter = Flipper::Adapters::ActiveSupportCacheStore.new( + adapter, + Rails.cache, + expires_in: 10.seconds) + + Flipper.new(cached_adapter) + end +end - Feature.register_feature_groups +Feature.register_feature_groups + +unless Rails.env.test? + require 'flipper/middleware/memoizer' + Rails.application.config.middleware.use Flipper::Middleware::Memoizer end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index ba4481ae602..0f164e628f9 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -42,6 +42,8 @@ Sidekiq.configure_server do |config| Gitlab::SidekiqThrottler.execute! + Gitlab::SidekiqVersioning.install! + config = Gitlab::Database.config || Rails.application.config.database_configuration[Rails.env] config['pool'] = Sidekiq.options[:concurrency] @@ -60,19 +62,3 @@ Sidekiq.configure_client do |config| chain.add Gitlab::SidekiqStatus::ClientMiddleware end end - -# The Sidekiq client API always adds the queue to the Sidekiq queue -# list, but mail_room and gitlab-shell do not. This is only necessary -# for monitoring. -begin - queues = Gitlab::SidekiqConfig.worker_queues - - Sidekiq.redis do |conn| - conn.pipelined do - queues.each do |queue| - conn.sadd('queues', queue) - end - end - end -rescue Redis::BaseError, SocketError, Errno::ENOENT, Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED -end diff --git a/config/no_todos_messages.yml b/config/no_todos_messages.yml index 264a975b614..da721a9b6e6 100644 --- a/config/no_todos_messages.yml +++ b/config/no_todos_messages.yml @@ -3,9 +3,9 @@ # # If you come up with a fun one, please feel free to contribute it to GitLab! # https://about.gitlab.com/contributing/ ---- -- Good job! Looks like you don't have any todos left. +--- +- Good job! Looks like you don't have any todos left - Isn't an empty todo list beautiful? - Give yourself a pat on the back! - Nothing left to do, high five! -- Henceforth you shall be known as "Todo Destroyer". +- Henceforth you shall be known as "Todo Destroyer" diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index bc7c431731a..31a38f2b508 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -25,8 +25,6 @@ - [new_note, 2] - [new_issue, 2] - [new_merge_request, 2] - - [build, 2] - - [pipeline, 2] - [pipeline_processing, 5] - [pipeline_creation, 4] - [pipeline_default, 3] @@ -38,11 +36,12 @@ - [mailers, 2] - [invalid_gpg_signature_update, 2] - [create_gpg_signature, 2] + - [rebase, 2] - [upload_checksum, 1] - [repository_fork, 1] - [repository_import, 1] - [github_importer, 1] - - [github_importer_advance_stage, 1] + - [github_import_advance_stage, 1] - [project_service, 1] - [delete_user, 1] - [delete_merged_branches, 1] diff --git a/db/post_migrate/20171213160445_migrate_github_importer_advance_stage_sidekiq_queue.rb b/db/post_migrate/20171213160445_migrate_github_importer_advance_stage_sidekiq_queue.rb new file mode 100644 index 00000000000..149c28f1946 --- /dev/null +++ b/db/post_migrate/20171213160445_migrate_github_importer_advance_stage_sidekiq_queue.rb @@ -0,0 +1,16 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateGithubImporterAdvanceStageSidekiqQueue < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + sidekiq_queue_migrate 'github_importer_advance_stage', to: 'github_import_advance_stage' + end + + def down + sidekiq_queue_migrate 'github_import_advance_stage', to: 'github_importer_advance_stage' + end +end diff --git a/db/schema.rb b/db/schema.rb index f0b1da16d53..2048c50f892 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171206221519) do +ActiveRecord::Schema.define(version: 20171213160445) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index ee9b9a9466a..373d4239f71 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -14,3 +14,4 @@ providers. - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS - [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider - [Okta](okta.md) Configure GitLab to sign in using Okta +- [Authentiq](authentiq.md): Enable the Authentiq OmniAuth provider for passwordless authentication diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index a88e67bfeb5..ea8077f0623 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -37,6 +37,7 @@ Follow the steps below to configure an active/active setup: 1. [Configure the database](database.md) 1. [Configure Redis](redis.md) + 1. [Configure Redis for GitLab source installations](redis_source.md) 1. [Configure NFS](nfs.md) 1. [Configure the GitLab application servers](gitlab.md) 1. [Configure the load balancers](load_balancer.md) diff --git a/doc/administration/index.md b/doc/administration/index.md index e6986a2b07f..58922b71ae7 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -16,21 +16,27 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Install](../install/README.md): Requirements, directory structures, and installation methods. - [High Availability](high_availability/README.md): Configure multiple servers for scaling or high availability. + - [High Availability on AWS](../university/high-availability/aws/README.md): Set up GitLab HA on Amazon AWS. ### Configuring GitLab - [Adjust your instance's timezone](../workflow/timezone.md): Customize the default time zone of GitLab. -- [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers. -- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page. - [System hooks](../system_hooks/system_hooks.md): Notifications when users, projects and keys are changed. - [Security](../security/README.md): Learn what you can do to further secure your GitLab instance. - [Usage statistics, version check, and usage ping](../user/admin_area/settings/usage_statistics.md): Enable or disable information about your instance to be sent to GitLab, Inc. - [Polling](polling.md): Configure how often the GitLab UI polls for updates. - [GitLab Pages configuration](pages/index.md): Enable and configure GitLab Pages. -- [GitLab Pages configuration for installations from the source](pages/source.md): Enable and configure GitLab Pages on +- [GitLab Pages configuration for GitLab source installations](pages/source.md): Enable and configure GitLab Pages on [source installations](../install/installation.md#installation-from-source). - [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab. +#### Customizing GitLab's appearance + +- [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers. +- [Branded login page](../customization/branded_login_page.md): Customize the login page with your own logo, title, and description. +- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page. +- ["New Project" page](../customization/new_project_page.md): Customize the text to be displayed on the page that opens whenever your users create a new project. + ### Maintaining GitLab - [Raketasks](../raketasks/README.md): Perform various tasks for maintenance, backups, automatic webhooks setup, etc. @@ -74,6 +80,7 @@ server with IMAP authentication on Ubuntu, to be used with Reply by email. - [Issue closing pattern](issue_closing_pattern.md): Customize how to close an issue from commit messages. - [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service. - [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project. +- [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet. ### Repository settings diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md index 136192191f9..ecf92c379fd 100644 --- a/doc/administration/raketasks/maintenance.md +++ b/doc/administration/raketasks/maintenance.md @@ -221,3 +221,22 @@ sudo gitlab-rake gitlab:shell:create_hooks cd /home/git/gitlab sudo -u git -H bundle exec rake gitlab:shell:create_hooks RAILS_ENV=production ``` + +## Check TCP connectivity to a remote site + +Sometimes you need to know if your GitLab installation can connect to a TCP +service on another machine - perhaps a PostgreSQL or HTTPS server. A rake task +is included to help you with this: + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:tcp_check[example.com,80] +``` + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:tcp_check[example.com,80] RAILS_ENV=production +``` diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md index 1304476e678..3a2cced37bf 100644 --- a/doc/administration/reply_by_email.md +++ b/doc/administration/reply_by_email.md @@ -89,9 +89,11 @@ email address in order to sign up. If you also host a public-facing GitLab instance at `hooli.com` and set your incoming email domain to `hooli.com`, an attacker could abuse the "Create new -issue by email" feature by using a project's unique address as the email when -signing up for Slack, which would send a confirmation email, which would create -a new issue on the project owned by the attacker, allowing them to click the +issue by email" or +"[Create new merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email)" +features by using a project's unique address as the email when signing up for +Slack, which would send a confirmation email, which would create a new issue or +merge request on the project owned by the attacker, allowing them to click the confirmation link and validate their account on your company's private Slack instance. diff --git a/doc/api/issues.md b/doc/api/issues.md index ec8ff3cd3f3..d2fefbe68aa 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -514,9 +514,9 @@ PUT /projects/:id/issues/:issue_iid | `title` | string | no | The title of an issue | | `description` | string | no | The description of an issue | | `confidential` | boolean | no | Updates an issue to be confidential | -| `assignee_ids` | Array[integer] | no | The ID of the users to assign the issue to | -| `milestone_id` | integer | no | The ID of a milestone to assign the issue to | -| `labels` | string | no | Comma-separated label names for an issue | +| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. | +| `milestone_id` | integer | no | The ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.| +| `labels` | string | no | Comma-separated label names for an issue. Set to an empty string to unassign all labels. | | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | | `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index b2e4b6d0955..880b0ed2c65 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -524,15 +524,15 @@ PUT /projects/:id/merge_requests/:merge_request_iid | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `merge_request_iid` | integer | yes | The ID of a merge request | | `target_branch` | string | no | The target branch | | `title` | string | no | Title of MR | -| `assignee_id` | integer | no | Assignee user ID | +| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. | +| `milestone_id` | integer | no | The ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| +| `labels` | string | no | Comma-separated label names for an merge request. Set to an empty string to unassign all labels. | | `description` | string | no | Description of MR | | `state_event` | string | no | New state (close/reopen) | -| `labels` | string | no | Labels for MR as a comma-separated list | -| `milestone_id` | integer | no | The ID of a milestone | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | | `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. | diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 594babc74be..03b32577872 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -182,6 +182,8 @@ GET /projects/:id/repository/contributors Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user +- `order_by` (optional) - Return contributors ordered by `name`, `email`, or `commits` fields. If not given contributors are ordered by commit date. +- `sort` (optional) - Return contributors sorted in `asc` or `desc` order. Default is `asc` Response: diff --git a/doc/api/tags.md b/doc/api/tags.md index bebe6536b6e..fa25dc76452 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -12,7 +12,11 @@ GET /projects/:id/repository/tags Parameters: -- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string| yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user| +| `order_by` | string | no | Return tags ordered by `name` or `updated` fields. Default is `updated` | +| `sort` | string | no | Return tags sorted in `asc` or `desc` order. Default is `desc` | ```json [ diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index ecb8f15c851..fb5bfe26bb0 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -319,45 +319,62 @@ As you can see, the syntax of `command` is similar to [Dockerfile's `CMD`][cmd]. > Introduced in GitLab and GitLab Runner 9.4. Read more about the [extended configuration options](#extended-docker-configuration-options). +Before showing the available entrypoint override methods, let's describe shortly +how the Runner starts and uses a Docker image for the containers used in the +CI jobs: + +1. The Runner starts a Docker container using the defined entrypoint (default + from `Dockerfile` that may be overridden in `.gitlab-ci.yml`) +1. The Runner attaches itself to a running container. +1. The Runner prepares a script (the combination of + [`before_script`](../yaml/README.md#before_script), + [`script`](../yaml/README.md#script), + and [`after_script`](../yaml/README.md#after_script)). +1. The Runner sends the script to the container's shell STDIN and receives the + output. + +To override the entrypoint of a Docker image, the recommended solution is to +define an empty `entrypoint` in `.gitlab-ci.yml`, so the Runner doesn't start +a useless shell layer. However, that will not work for all Docker versions, and +you should check which one your Runner is using. Specifically: + +- If Docker 17.06 or later is used, the `entrypoint` can be set to an empty value. +- If Docker 17.03 or previous versions are used, the `entrypoint` can be set to + `/bin/sh -c`, `/bin/bash -c` or an equivalent shell available in the image. + +The syntax of `image:entrypoint` is similar to [Dockerfile's `ENTRYPOINT`][entrypoint]. + +---- + Let's assume you have a `super/sql:experimental` image with some SQL database inside it and you would like to use it as a base image for your job because you want to execute some tests with this database binary. Let's also assume that this image is configured with `/usr/bin/super-sql run` as an entrypoint. That -means, that when starting the container without additional options, it will run +means that when starting the container without additional options, it will run the database's process, while Runner expects that the image will have no -entrypoint or at least will start with a shell as its entrypoint. - -Before the new extended Docker configuration options, you would need to create -your own image based on the `super/sql:experimental` image, set the entrypoint -to a shell and then use it in job's configuration, like: +entrypoint or that the entrypoint is prepared to start a shell command. -```Dockerfile -# my-super-sql:experimental image's Dockerfile +With the extended Docker configuration options, instead of creating your +own image based on `super/sql:experimental`, setting the `ENTRYPOINT` +to a shell, and then using the new image in your CI job, you can now simply +define an `entrypoint` in `.gitlab-ci.yml`. -FROM super/sql:experimental -ENTRYPOINT ["/bin/sh"] -``` +**For Docker 17.06+:** ```yaml -# .gitlab-ci.yml - -image: my-super-sql:experimental +image: + name: super/sql:experimental + entrypoint: [""] ``` -After the new extended Docker configuration options, you can now simply -set an `entrypoint` in `.gitlab-ci.yml`, like: +**For Docker =< 17.03:** ```yaml -# .gitlab-ci.yml - image: name: super/sql:experimental - entrypoint: ["/bin/sh"] + entrypoint: ["/bin/sh", "-c"] ``` -As you can see the syntax of `entrypoint` is similar to -[Dockerfile's `ENTRYPOINT`][entrypoint]. - ## Define image and services in `config.toml` Look for the `[runners.docker]` section: diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index e5a2bbd1773..df0e1521150 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -1,84 +1,106 @@ -# Using SSH keys +--- +last_updated: 2017-12-13 +--- + +# Using SSH keys with GitLab CI/CD GitLab currently doesn't have built-in support for managing SSH keys in a build -environment. +environment (where the GitLab Runner runs). The SSH keys can be useful when: 1. You want to checkout internal submodules -2. You want to download private packages using your package manager (eg. bundler) -3. You want to deploy your application to eg. Heroku or your own server -4. You want to execute SSH commands from the build server to the remote server -5. You want to rsync files from your build server to the remote server +1. You want to download private packages using your package manager (e.g., Bundler) +1. You want to deploy your application to your own server, or, for example, Heroku +1. You want to execute SSH commands from the build environment to a remote server +1. You want to rsync files from the build environment to a remote server If anything of the above rings a bell, then you most likely need an SSH key. -## Inject keys in your build server - The most widely supported method is to inject an SSH key into your build -environment by extending your `.gitlab-ci.yml`. - -This is the universal solution which works with any type of executor -(docker, shell, etc.). - -### How it works - -1. Create a new SSH key pair with [ssh-keygen][] -2. Add the private key as a **Secret Variable** to the project -3. Run the [ssh-agent][] during job to load the private key. +environment by extending your `.gitlab-ci.yml`, and it's a solution which works +with any type of [executor](https://docs.gitlab.com/runner/executors/) +(Docker, shell, etc.). + +## How it works + +1. Create a new SSH key pair locally with [ssh-keygen](http://linux.die.net/man/1/ssh-keygen) +1. Add the private key as a [secret variable](../variables/README.md) to + your project +1. Run the [ssh-agent](http://linux.die.net/man/1/ssh-agent) during job to load + the private key. +1. Copy the public key to the servers you want to have access to (usually in + `~/.ssh/authorized_keys`) or add it as a [deploy key](../../ssh/README.md#deploy-keys) + if you are accessing a private GitLab repository. + +NOTE: **Note:** +The private key will not be displayed in the job trace, unless you enable +[debug tracing](../variables/README.md#debug-tracing). You might also want to +check the [visibility of your pipelines](../../user/project/pipelines/settings.md#visibility-of-pipelines). ## SSH keys when using the Docker executor -You will first need to create an SSH key pair. For more information, follow the -instructions to [generate an SSH key](../../ssh/README.md). Do not add a -passphrase to the SSH key, or the `before_script` will prompt for it. - -Then, create a new **Secret Variable** in your project settings on GitLab -following **Settings > CI/CD** and look for the "Secret Variables" section. -As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the -content of your _private_ key that you created earlier. - -It is also good practice to check the server's own public key to make sure you -are not being targeted by a man-in-the-middle attack. To do this, add another -variable named `SSH_SERVER_HOSTKEYS`. To find out the hostkeys of your server, run -the `ssh-keyscan YOUR_SERVER` command from a trusted network (ideally, from the -server itself), and paste its output into the `SSH_SERVER_HOSTKEYS` variable. If -you need to connect to multiple servers, concatenate all the server public keys -that you collected into the **Value** of the variable. There must be one key per -line. - -Next you need to modify your `.gitlab-ci.yml` with a `before_script` action. -Add it to the top: - -``` -before_script: - # Install ssh-agent if not already installed, it is required by Docker. - # (change apt-get to yum if you use a CentOS-based image) - - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' - - # Run ssh-agent (inside the build environment) - - eval $(ssh-agent -s) - - # Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store - - ssh-add <(echo "$SSH_PRIVATE_KEY") - - # For Docker builds disable host key checking. Be aware that by adding that - # you are suspectible to man-in-the-middle attacks. - # WARNING: Use this only with the Docker executor, if you use it with shell - # you will overwrite your user's SSH config. - - mkdir -p ~/.ssh - - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' - # In order to properly check the server's host key, assuming you created the - # SSH_SERVER_HOSTKEYS variable previously, uncomment the following two lines - # instead. - # - mkdir -p ~/.ssh - # - '[[ -f /.dockerenv ]] && echo "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts' -``` - -As a final step, add the _public_ key from the one you created earlier to the -services that you want to have an access to from within the build environment. -If you are accessing a private GitLab repository you need to add it as a -[deploy key](../../ssh/README.md#deploy-keys). +When your CI/CD jobs run inside Docker containers (meaning the environment is +contained) and you want to deploy your code in a private server, you need a way +to access it. This is where an SSH key pair comes in handy. + +1. You will first need to create an SSH key pair. For more information, follow + the instructions to [generate an SSH key](../../ssh/README.md#generating-a-new-ssh-key-pair). + **Do not** add a passphrase to the SSH key, or the `before_script` will\ + prompt for it. + +1. Create a new [secret variable](../variables/README.md#secret-variables). + As **Key** enter the name `SSH_PRIVATE_KEY` and in the **Value** field paste + the content of your _private_ key that you created earlier. + +1. Modify your `.gitlab-ci.yml` with a `before_script` action. In the following + example, a Debian based image is assumed. Edit to your needs: + + ```yaml + before_script: + ## + ## Install ssh-agent if not already installed, it is required by Docker. + ## (change apt-get to yum if you use an RPM-based image) + ## + - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' + + ## + ## Run ssh-agent (inside the build environment) + ## + - eval $(ssh-agent -s) + + ## + ## Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store + ## We're using tr to fix line endings which makes ed25519 keys work + ## without extra base64 encoding. + ## https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556 + ## + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null + + ## + ## Create the SSH directory and give it the right permissions + ## + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + + ## + ## Optionally, if you will be using any Git commands, set the user name and + ## and email. + ## + #- git config --global user.email "user@example.com" + #- git config --global user.name "User name" + ``` + + NOTE: **Note:** + The [`before_script`](../yaml/README.md#before-script) can be set globally + or per-job. + +1. Make sure the private server's [SSH host keys are verified](#verifying-the-ssh-host-keys). + +1. As a final step, add the _public_ key from the one you created in the first + step to the services that you want to have an access to from within the build + environment. If you are accessing a private GitLab repository you need to add + it as a [deploy key](../../ssh/README.md#deploy-keys). That's it! You can now have access to private servers or repositories in your build environment. @@ -91,24 +113,93 @@ SSH key. You can generate the SSH key from the machine that GitLab Runner is installed on, and use that key for all projects that are run on this machine. -First, you need to login to the server that runs your jobs. +1. First, you need to login to the server that runs your jobs. + +1. Then from the terminal login as the `gitlab-runner` user: -Then from the terminal login as the `gitlab-runner` user and generate the SSH -key pair as described in the [SSH keys documentation](../../ssh/README.md). + ``` + sudo su - gitlab-runner + ``` -As a final step, add the _public_ key from the one you created earlier to the -services that you want to have an access to from within the build environment. -If you are accessing a private GitLab repository you need to add it as a -[deploy key](../../ssh/README.md#deploy-keys). +1. Generate the SSH key pair as described in the instructions to + [generate an SSH key](../../ssh/README.md#generating-a-new-ssh-key-pair). + **Do not** add a passphrase to the SSH key, or the `before_script` will + prompt for it. + +1. As a final step, add the _public_ key from the one you created earlier to the + services that you want to have an access to from within the build environment. + If you are accessing a private GitLab repository you need to add it as a + [deploy key](../../ssh/README.md#deploy-keys). Once done, try to login to the remote server in order to accept the fingerprint: ```bash -ssh <address-of-my-server> +ssh example.com +``` + +For accessing repositories on GitLab.com, you would use `git@gitlab.com`. + +## Verifying the SSH host keys + +It is a good practice to check the private server's own public key to make sure +you are not being targeted by a man-in-the-middle attack. In case anything +suspicious happens, you will notice it since the job would fail (the SSH +connection would fail if the public keys would not match). + +To find out the host keys of your server, run the `ssh-keyscan` command from a +trusted network (ideally, from the private server itself): + +```sh +## Use the domain name +ssh-keyscan example.com + +## Or use an IP +ssh-keyscan 1.2.3.4 ``` -For accessing repositories on GitLab.com, the `<address-of-my-server>` would be -`git@gitlab.com`. +Create a new [secret variable](../variables/README.md#secret-variables) with +`SSH_KNOWN_HOSTS` as "Key", and as a "Value" add the output of `ssh-keyscan`. + +NOTE: **Note:** +If you need to connect to multiple servers, all the server host keys +need to be collected in the **Value** of the variable, one key per line. + +TIP: **Tip:** +By using a secret variable instead of `ssh-keyscan` directly inside +`.gitlab-ci.yml`, it has the benefit that you don't have to change `.gitlab-ci.yml` +if the host domain name changes for some reason. Also, the values are predefined +by you, meaning that if the host keys suddenly change, the CI/CD job will fail, +and you'll know there's something wrong with the server or the network. + +Now that the `SSH_KNOWN_HOSTS` variable is created, in addition to the +[content of `.gitlab-ci.yml`](#ssh-keys-when-using-the-docker-executor) +above, here's what more you need to add: + + ```yaml +before_script: + ## + ## Assuming you created the SSH_KNOWN_HOSTS variable, uncomment the + ## following two lines. + ## + - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts' + - chmod 644 ~/.ssh/known_hosts + + ## + ## Alternatively, use ssh-keyscan to scan the keys of your private server. + ## Replace example.com with your private server's domain name. Repeat that + ## command if you have more than one server to connect to. + ## + #- ssh-keyscan example.com >> ~/.ssh/known_hosts + #- chmod 644 ~/.ssh/known_hosts + + ## + ## You can optionally disable host key checking. Be aware that by adding that + ## you are suspectible to man-in-the-middle attacks. + ## WARNING: Use this only with the Docker executor, if you use it with shell + ## you will overwrite your user's SSH config. + ## + #- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' +``` ## Example project @@ -119,6 +210,4 @@ that runs on [GitLab.com](https://gitlab.com) using our publicly available Want to hack on it? Simply fork it, commit and push your changes. Within a few moments the changes will be picked by a public runner and the job will begin. -[ssh-keygen]: http://linux.die.net/man/1/ssh-keygen -[ssh-agent]: http://linux.die.net/man/1/ssh-agent [ssh-example-repo]: https://gitlab.com/gitlab-examples/ssh-private-key/ diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index a9e6bda9916..b9d4a2098ed 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -213,14 +213,15 @@ An example project service that defines deployment variables is ## Debug tracing > Introduced in GitLab Runner 1.7. -> -> **WARNING:** Enabling debug tracing can have severe security implications. The - output **will** contain the content of all your secret variables and any other - secrets! The output **will** be uploaded to the GitLab server and made visible - in job traces! + +CAUTION: **Warning:** +Enabling debug tracing can have severe security implications. The +output **will** contain the content of all your secret variables and any other +secrets! The output **will** be uploaded to the GitLab server and made visible +in job traces! By default, GitLab Runner hides most of the details of what it is doing when -processing a job. This behaviour keeps job traces short, and prevents secrets +processing a job. This behavior keeps job traces short, and prevents secrets from being leaked into the trace unless your script writes them to the screen. If a job isn't working as expected, this can make the problem difficult to diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index 31164ccd465..d14ba6ad522 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -1,3 +1,7 @@ +--- +comments: false +--- + This document was split into: - [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md). diff --git a/doc/customization/welcome_message.md b/doc/customization/welcome_message.md index a0cb234bea0..0aef0bf5abb 100644 --- a/doc/customization/welcome_message.md +++ b/doc/customization/welcome_message.md @@ -8,5 +8,5 @@ It is possible to add a markdown-formatted welcome message to your GitLab sign-in page. Users of GitLab Enterprise Edition should use the [branded login page feature](branded_login_page.md) instead. -The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI. +The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI. Admin area > Settings diff --git a/doc/development/fe_guide/dropdowns.md b/doc/development/fe_guide/dropdowns.md index e1660ac5caa..6314f8f38d2 100644 --- a/doc/development/fe_guide/dropdowns.md +++ b/doc/development/fe_guide/dropdowns.md @@ -4,15 +4,15 @@ ## How to style a bootstrap dropdown 1. Use the HTML structure provided by the [docs][bootstrap-dropdowns] 1. Add a specific class to the top level `.dropdown` element - - + + ```Haml .dropdown.my-dropdown %button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false } %span.dropdown-toggle-text Toggle Dropdown = icon('chevron-down') - + %ul.dropdown-menu %li %a @@ -29,10 +29,4 @@ item! ``` -1. Include the mixin in CSS - - ```SCSS - @include new-style-dropdown('.my-dropdown '); - ``` - [bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md index 085fb8f902c..59ebf41e09f 100644 --- a/doc/development/sidekiq_style_guide.md +++ b/doc/development/sidekiq_style_guide.md @@ -9,25 +9,54 @@ All workers should include `ApplicationWorker` instead of `Sidekiq::Worker`, which adds some convenience methods and automatically sets the queue based on the worker's name. -## Default Queue +## Dedicated Queues -Use of the "default" queue is not allowed. Every worker should use a queue that -matches the worker's purpose the closest. For example, workers that are to be -executed periodically should use the "cronjob" queue. +All workers should use their own queue, which is automatically set based on the +worker class name. For a worker named `ProcessSomethingWorker`, the queue name +would be `process_something`. If you're not sure what queue a worker uses, +you can find it using `SomeWorker.queue`. There is almost never a reason to +manually override the queue name using `sidekiq_options queue: :some_queue`. -A list of all available queues can be found in `config/sidekiq_queues.yml`. +## Queue Namespaces -## Dedicated Queues +While different workers cannot share a queue, they can share a queue namespace. -Most workers should use their own queue, which is automatically set based on the -worker class name. For a worker named `ProcessSomethingWorker`, the queue name -would be `process_something`. If you're not sure what a worker's queue name is, -you can find it using `SomeWorker.queue`. +Defining a queue namespace for a worker makes it possible to start a Sidekiq +process that automatically handles jobs for all workers in that namespace, +without needing to explicitly list all their queue names. If, for example, all +workers that are managed by sidekiq-cron use the `cronjob` queue namespace, we +can spin up a Sidekiq process specifically for these kinds of scheduled jobs. +If a new worker using the `cronjob` namespace is added later on, the Sidekiq +process will automatically pick up jobs for that worker too (after having been +restarted), without the need to change any configuration. + +A queue namespace can be set using the `queue_namespace` DSL class method: + +```ruby +class SomeScheduledTaskWorker + include ApplicationWorker + + queue_namespace :cronjob + + # ... +end +``` + +Behind the scenes, this will set `SomeScheduledTaskWorker.queue` to +`cronjob:some_scheduled_task`. Commonly used namespaces will have their own +concern module that can easily be included into the worker class, and that may +set other Sidekiq options besides the queue namespace. `CronjobQueue`, for +example, sets the namespace, but also disables retries. + +`bundle exec sidekiq` is namespace-aware, and will automatically listen on all +queues in a namespace (technically: all queues prefixed with the namespace name) +when a namespace is provided instead of a simple queue name in the `--queue` +(`-q`) option, or in the `:queues:` section in `config/sidekiq_queues.yml`. -In some cases multiple workers do use the same queue. For example, the various -workers for updating CI pipelines all use the `pipeline` queue. Adding workers -to existing queues should be done with care, as adding more workers can lead to -slow jobs blocking work (even for different jobs) on the shared queue. +Note that adding a worker to an existing namespace should be done with care, as +the extra jobs will take resources away from jobs from workers that were already +there, if the resources available to the Sidekiq process handling the namespace +are not adjusted appropriately. ## Tests @@ -36,7 +65,7 @@ tests should be placed in `spec/workers`. ## Removing or renaming queues -Try to avoid renaming or removing queues in minor and patch releases. +Try to avoid renaming or removing workers and their queues in minor and patch releases. During online update instance can have pending jobs and removing the queue can lead to those jobs being stuck forever. If you can't write migration for those Sidekiq jobs, please consider doing rename or remove queue in major release only. diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md index 16a811dbc74..d396964e7c1 100644 --- a/doc/development/ux_guide/components.md +++ b/doc/development/ux_guide/components.md @@ -10,7 +10,7 @@ * [Tables](#tables) * [Blocks](#blocks) * [Panels](#panels) -* [Dialog modals](#dialog-modals) +* [Modals](#modals) * [Alerts](#alerts) * [Forms](#forms) * [Search box](#search-box) @@ -255,18 +255,18 @@ Skeleton loading can replace any existing UI elements for the period in which th --- -## Dialog modals +## Modals -Dialog modals are only used for having a conversation and confirmation with the user. The user is not able to access the features on the main page until closing the modal. +Modals are only used for having a conversation and confirmation with the user. The user is not able to access the features on the main page until closing the modal. ### Usage -* When the action is irreversible, dialog modals provide the details and confirm with the user before they take an advanced action. -* When the action will affect privacy or authorization, dialog modals provide advanced information and confirm with the user. +* When the action is irreversible, modals provide the details and confirm with the user before they take an advanced action. +* When the action will affect privacy or authorization, modals provide advanced information and confirm with the user. ### Style -* Dialog modals contain the header, body, and actions. +* Modals contain the header, body, and actions. * **Header(1):** The header title is a question instead of a descriptive phrase. * **Body(2):** The content in body should never be ambiguous and unclear. It provides specific information. * **Actions(3):** Contains a affirmative action, a dismissive action, and an extra action. The order of actions from left to right: Dismissive action → Extra action → Affirmative action @@ -277,13 +277,13 @@ Dialog modals are only used for having a conversation and confirmation with the ### Placement -* Dialog modals should always be the center of the screen horizontally and be positioned **72px** from the top. +* Modals should always be the center of the screen horizontally and be positioned **72px** from the top. -| Dialog with 2 actions | Dialog with 3 actions | Special confirmation | +| Modal with 2 actions | Modal with 3 actions | Special confirmation | | --------------------- | --------------------- | -------------------- | | ![two-actions](img/modals-general-confimation-dialog.png) | ![three-actions](img/modals-three-buttons.png) | ![spcial-confirmation](img/modals-special-confimation-dialog.png) | -> TODO: Special case for dialog modal. +> TODO: Special case for modal. --- diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md index 12e8d0a31bb..af842da7f62 100644 --- a/doc/development/ux_guide/copy.md +++ b/doc/development/ux_guide/copy.md @@ -46,11 +46,11 @@ Avoid using periods in solitary sentences in these elements: * Labels
* Hover text
* Bulleted lists
-* Dialog body text
+* Modal body text
Periods should be used for:
-* Lists or dialogs with multiple sentences
+* Lists or modals with multiple sentences
* Any sentence followed by a link
| :white_check_mark: **Do** place periods after sentences followed by a link | :no_entry_sign: **Don’t** place periods after a link if it‘s not followed by a sentence |
@@ -80,7 +80,7 @@ Omit punctuation after phrases and labels to create a cleaner and more readable | Punctuation mark | Copy and paste | HTML entity | Unicode | Mac shortcut | Windows shortcut | Description |
|---|---|---|---|---|---|---|
-| Period | **.** | | | | | Omit for single sentences in affordances like labels, hover text, bulleted lists, and dialog body text.<br><br>Use in lists or dialogs with multiple sentences, and any sentence followed by a link or inline code.<br><br>Place inside quotation marks unless you’re telling the reader what to enter and it’s ambiguous whether to include the period. |
+| Period | **.** | | | | | Omit for single sentences in affordances like labels, hover text, bulleted lists, and modal body text.<br><br>Use in lists or modals with multiple sentences, and any sentence followed by a link or inline code.<br><br>Place inside quotation marks unless you’re telling the reader what to enter and it’s ambiguous whether to include the period. |
| Comma | **,** | | | | | Place inside quotation marks.<br><br>Use a [serial comma][serial comma] in lists of three or more terms. |
| Exclamation point | **!** | | | | | Avoid exclamation points as they tend to come across as shouting. Some exceptions include greetings or congratulatory messages. |
| Colon | **:** | `:` | `\u003A` | | | Omit from labels, for example, in the labels for fields in a form. |
@@ -88,7 +88,7 @@ Omit punctuation after phrases and labels to create a cleaner and more readable | Quotation marks | **“**<br><br>**”**<br><br>**‘**<br><br>**’** | `“`<br><br>`”`<br><br>`‘`<br><br>`’` | `\u201C`<br><br>`\u201D`<br><br>`\u2018`<br><br>`\u2019` | <kbd>⌥ Option</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>]</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 7</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 8</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 5</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use proper quotation marks (also known as smart quotes, curly quotes, or typographer’s quotes) for quotes. Single quotation marks are used for quotes inside of quotes.<br><br>The right single quotation mark symbol is also used for apostrophes.<br><br>Don’t use primes, straight quotes, or free-standing accents for quotation marks. |
| Primes | **′**<br><br>**″** | `′`<br><br>`″` | `\u2032`<br><br>`\u2033` | | <kbd>Alt</kbd>+<kbd>8 2 4 2</kbd><br><br><kbd>Alt</kbd>+<kbd>8 2 4 3</kbd> | Use prime (′) only in abbreviations for feet, arcminutes, and minutes: 3° 15′<br><br>Use double-prime (″) only in abbreviations for inches, arcseconds, and seconds: 3° 15′ 35″<br><br>Don’t use quotation marks, straight quotes, or free-standing accents for primes. |
| Straight quotes and accents | **"**<br><br>**'**<br><br>**`**<br><br>**´** | `"`<br><br>`'`<br><br>```<br><br>`´` | `\u0022`<br><br>`\u0027`<br><br>`\u0060`<br><br>`\u00B4` | | | Don’t use straight quotes or free-standing accents for primes or quotation marks.<br><br>Proper typography never uses straight quotes. They are left over from the age of typewriters and their only modern use is for code. |
-| Ellipsis | **…** | `…` | | <kbd>⌥ Option</kbd>+<kbd>;</kbd> | <kbd>Alt</kbd>+<kbd>0 1 3 3</kbd> | Use to indicate an action in progress (“Downloading…”) or incomplete or truncated text. No space before the ellipsis.<br><br>Omit from menu items or buttons that open a dialog or start some other process. |
+| Ellipsis | **…** | `…` | | <kbd>⌥ Option</kbd>+<kbd>;</kbd> | <kbd>Alt</kbd>+<kbd>0 1 3 3</kbd> | Use to indicate an action in progress (“Downloading…”) or incomplete or truncated text. No space before the ellipsis.<br><br>Omit from menu items or buttons that open a modal or start some other process. |
| Chevrons | **«**<br><br>**»**<br><br>**‹**<br><br>**›**<br><br>**<**<br><br>**>** | `«`<br><br>`»`<br><br>`‹`<br><br>`›`<br><br>`<`<br><br>`>` | `\u00AB`<br><br>`\u00BB`<br><br>`\u2039`<br><br>`\u203A`<br><br>`\u003C`<br><br>`\u003E`<br><br> | | | Omit from links or buttons that open another page or move to the next or previous step in a process. Also known as angle brackets, angular quote brackets, or guillemets. |
| Em dash | **—** | `—` | `\u2014` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 1</kbd> | Avoid using dashes to separate text. If you must use dashes for this purpose — like this — use an em dash surrounded by spaces. |
| En dash | **–** | `–` | `\u2013` | <kbd>⌥ Option</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 0</kbd> | Use an en dash without spaces instead of a hyphen to indicate a range of values, such as numbers, times, and dates: “3–5 kg”, “8:00 AM–12:30 PM”, “10–17 Jan” |
@@ -175,7 +175,7 @@ A **comment** is a written piece of text that users of GitLab can create. Commen #### Discussion
A **discussion** is a group of 1 or more comments. A discussion can include subdiscussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved.
-## Confirmation dialogs
+## Modals
- Destruction buttons should be clear and always say what they are destroying.
E.g., `Delete page` instead of just `Delete`.
@@ -184,6 +184,8 @@ A **discussion** is a group of 1 or more comments. A discussion can include subd - Avoid the word `cancel` or `canceled` in the descriptive copy. It can be
confusing when you then see the `Cancel` button.
+see also: guidelines for [modal components](components.md#modals)
+
---
Portions of this page are modifications based on work created and shared by the [Android Open Source Project][android project] and used according to terms described in the [Creative Commons 2.5 Attribution License][creative commons].
diff --git a/doc/install/installation.md b/doc/install/installation.md index 570b0d5b22f..6c6e5db4cd9 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -299,9 +299,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-2-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-3-stable gitlab -**Note:** You can change `10-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `10-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md index df56f031970..588f4fa369f 100644 --- a/doc/topics/git/index.md +++ b/doc/topics/git/index.md @@ -61,6 +61,10 @@ We've gathered some resources to help you to get the best from Git with GitLab. - [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/) - [Towards a production quality open source Git LFS server](https://about.gitlab.com/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/) +## Troubleshooting + +- Learn a few [Git troubleshooting](troubleshooting_git.md) techniques to help you out. + ## General information - **Articles:** diff --git a/doc/topics/git/troubleshooting_git.md b/doc/topics/git/troubleshooting_git.md new file mode 100644 index 00000000000..8555c5e91ea --- /dev/null +++ b/doc/topics/git/troubleshooting_git.md @@ -0,0 +1,82 @@ +# Troubleshooting Git + +Sometimes things don't work the way they should or as you might expect when +you're using Git. Here are some tips on troubleshooting and resolving issues +with Git. + +## Broken pipe errors on git push + +'Broken pipe' errors can occur when attempting to push to a remote repository. +When pushing you will usually see: + +``` +Write failed: Broken pipe +fatal: The remote end hung up unexpectedly +``` + +To fix this issue, here are some possible solutions. + +### Increase the POST buffer size in Git + +**If pushing over HTTP**, you can try increasing the POST buffer size in Git's +configuration. Open a terminal and enter: + +```sh +git config http.postBuffer 52428800 +``` + +The value is specified in bytes, so in the above case the buffer size has been +set to 50MB. The default is 1MB. + +### Check your SSH configuration + +**If pushing over SSH**, first check your SSH configuration as 'Broken pipe' +errors can sometimes be caused by underlying issues with SSH (such as +authentication). Make sure that SSH is correctly configured by following the +instructions in the [SSH troubleshooting] docs. + +There's another option where you can prevent session timeouts by configuring +SSH 'keep alive' either on the client or on the server (if you are a GitLab +admin and have access to the server). + +NOTE: **Note:** configuring *both* the client and the server is unnecessary. + +**To configure SSH on the client side**: + +- On UNIX, edit `~/.ssh/config` (create the file if it doesn’t exist) and + add or edit: + + ``` + Host your-gitlab-instance-url.com + ServerAliveInterval 60 + ServerAliveCountMax 5 + ``` + +- On Windows, if you are using PuTTY, go to your session properties, then + navigate to "Connection" and under "Sending of null packets to keep + session active", set "Seconds between keepalives (0 to turn off)" to `60`. + +**To configure SSH on the server side**, edit `/etc/ssh/sshd_config` and add: + +``` +ClientAliveInterval 60 +ClientAliveCountMax 5 +``` + +### Running a git repack + +**If 'pack-objects' type errors are also being displayed**, you can try to +run a `git repack` before attempting to push to the remote repository again: + +```sh +git repack +git push +``` + +### Upgrade your Git client + +In case you're running an older version of Git (< 2.9), consider upgrading +to >= 2.9 (see [Broken pipe when pushing to Git repository][Broken-Pipe]). + +[SSH troubleshooting]: ../../ssh/README.md#troubleshooting "SSH Troubleshooting" +[Broken-Pipe]: https://stackoverflow.com/questions/19120120/broken-pipe-when-pushing-to-git-repository/36971469#36971469 "StackOverflow: 'Broken pipe when pushing to Git repository'" diff --git a/doc/update/10.2-to-10.3.md b/doc/update/10.2-to-10.3.md new file mode 100644 index 00000000000..07f9ee965f0 --- /dev/null +++ b/doc/update/10.2-to-10.3.md @@ -0,0 +1,360 @@ +--- +comments: false +--- + +# From 10.2 to 10.3 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz +echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz +cd ruby-2.3.5 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + + +Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage +JavaScript dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go +1.5.x through 1.7.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.8.3.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-3-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-3-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 8. Update gitlab-workhorse + +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 9. Update Gitaly + +#### New Gitaly configuration options required + +In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`. + +```shell +echo ' +[gitaly-ruby] +dir = "/home/git/gitaly/ruby" + +[gitlab-shell] +dir = "/home/git/gitlab-shell" +' | sudo -u git tee -a /home/git/gitaly/config.toml +``` + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" +``` + +If you use MySQL with replication, or just have MySQL configured with binary logging, +you will need to also run the following on all of your MySQL servers: + +```bash +mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;" +``` + +You can make this setting permanent by adding it to your `my.cnf`: + +``` +log_bin_trust_function_creators=1 +``` + +### 11. Update configuration files + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/10-2-stable:config/gitlab.yml.example origin/10-3-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/10-2-stable:lib/support/nginx/gitlab-ssl origin/10-3-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/10-2-stable:lib/support/nginx/gitlab origin/10-3-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/10-2-stable:lib/support/init.d/gitlab.default.example origin/10-3-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 12. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 13. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 14. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (10.0) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 9.5 to 10.0](9.5-to-10.0.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/user/group/index.md b/doc/user/group/index.md index a1671f9dd91..1733017cbc0 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -197,11 +197,11 @@ username, you can create a new group and transfer projects to it. Changing a group's path can have unintended side effects. * Existing web URLs for the group and anything under it (i.e. projects) will -redirect to the new URLs -* Existing Git remote URLs for projects under the group will no longer work, but -Git responses will show an error with the new remote URL -* The original namespace can be claimed again by any group or user, which will -destroy web redirects and Git remote warnings +redirect to the new URLs. +* Existing Git remote URLs for projects under the group will redirect to the new remote URL, and they +will show a warning with the new remote URL. +* The redirect to the new URL is permanent, that implies the original namespace +can't be claimed again by any group or user. * If you are vacating the path so it can be claimed by another group or user, you may need to rename the group name as well since both names and paths must be unique diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index 04e615330ce..dae4cbe170b 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -45,11 +45,10 @@ Alternatively, you can follow [this detailed procedure from the GitLab Team Hand Changing your username can have unintended side effects. * Existing web URLs for the user and anything under it (i.e. projects) will -redirect to the new URLs -* Existing Git remote URLs for projects under the user will no longer work, but -Git responses will show an error with the new remote URL -* The original namespace can be claimed again by any group or user, which will -destroy any web redirects and Git remote warnings +redirect to the new URLs. +* Existing Git remote URLs for projects under the user will redirect to the new remote URL. Git responses +will show a warning with the new remote URL. +* The redirect to the new URL is permanent, that implies the original namespace can't be claimed again by any group or user. > It is currently not possible to rename a namespace if it contains a project with container registry tags, because the project cannot be moved. diff --git a/doc/user/project/merge_requests/img/create_from_email.png b/doc/user/project/merge_requests/img/create_from_email.png Binary files differnew file mode 100644 index 00000000000..71eb4bf267d --- /dev/null +++ b/doc/user/project/merge_requests/img/create_from_email.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index b5c3f74a113..7037d7f5989 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -27,7 +27,7 @@ With GitLab merge requests, you can: - [Resolve merge conflicts from the UI](#resolve-conflicts) - Enable [fast-forward merge requests](#fast-forward-merge-requests) - Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch -- [Create new merge requests by email](#create_by_email) +- [Create new merge requests by email](#create-new-merge-requests-by-email) With **[GitLab Enterprise Edition][ee]**, you can also: @@ -139,7 +139,12 @@ address. The address can be obtained on the merge requests page by clicking on a **Email a new merge request to this project** button. The subject will be used as the source branch name for the new merge request and the target branch will be the default branch for the project. The message body (if not empty) -will be used as the merge request description. +will be used as the merge request description. You need +["Reply by email"](../../../administration/reply_by_email.md) enabled to use +this feature. If it's not enabled to your instance, you may ask your GitLab +administrator to do so. + +![Create new merge requests by email](img/create_from_email.png) ## Revert changes diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index daa5463d680..43451844f2d 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -68,7 +68,7 @@ in the pipelines settings page. Access to pipelines and job details (including output of logs and artifacts) is checked against your current user access level and the **Public pipelines** -project setting. +project setting under your project's **Settings > CI/CD > General pipelines settings**. If **Public pipelines** is enabled (default): diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index a234a647b77..2b6fde1e2a5 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -50,3 +50,9 @@ Here you can run housekeeping, archive, rename, transfer, or remove a project. It's possible to mark a project as archived via the Project Settings. An archived project will be hidden by default in the project listings. An archived project can be fully restored and will therefore retain it's repository and all associated resources whilst in an archived state. + +#### Renaming a project + +>**Note:** Only Project Owners and Admin users have the permission to rename a project + +It's possible to rename a project from "Rename repository" or "Transfer project" sections. When doing so, you will need to update your local repositories to point to the new location, otherwise Git operations will be rejected. diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 7887b886c03..4f36bbd760f 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -110,10 +110,12 @@ module API end params do use :pagination + optional :order_by, type: String, values: %w[email name commits], default: nil, desc: 'Return contributors ordered by `name` or `email` or `commits`' + optional :sort, type: String, values: %w[asc desc], default: nil, desc: 'Sort by asc (ascending) or desc (descending)' end get ':id/repository/contributors' do begin - contributors = ::Kaminari.paginate_array(user_project.repository.contributors) + contributors = ::Kaminari.paginate_array(user_project.repository.contributors(order_by: params[:order_by], sort: params[:sort])) present paginate(contributors), with: Entities::Contributor rescue not_found! diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 0d394a7b441..5e0afc6a7e4 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -14,10 +14,15 @@ module API success Entities::Tag end params do + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return tags sorted in updated by `asc` or `desc` order.' + optional :order_by, type: String, values: %w[name updated], default: 'updated', + desc: 'Return tags ordered by `name` or `updated` fields.' use :pagination end get ':id/repository/tags' do - tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse) + tags = ::Kaminari.paginate_array(::TagsFinder.new(user_project.repository, sort: "#{params[:order_by]}_#{params[:sort]}").execute) + present paginate(tags), with: Entities::Tag, project: user_project end diff --git a/lib/feature.rb b/lib/feature.rb index ac3bc65c0d5..8e9ba5c530a 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -1,5 +1,3 @@ -require 'flipper/adapters/active_record' - class Feature # Classes to override flipper table names class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature @@ -62,12 +60,7 @@ class Feature end def flipper - @flipper ||= begin - adapter = Flipper::Adapters::ActiveRecord.new( - feature_class: FlipperFeature, gate_class: FlipperGate) - - Flipper.new(adapter) - end + @flipper ||= Flipper.instance end # This method is called from config/initializers/flipper.rb and can be used diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb index fb28e80ff73..b9099ce256a 100644 --- a/lib/gitlab/conflict/file_collection.rb +++ b/lib/gitlab/conflict/file_collection.rb @@ -19,6 +19,8 @@ module Gitlab commit_message: commit_message || default_commit_message } resolver.resolve_conflicts(user, files, args) + ensure + @merge_request.clear_memoized_shas end def files diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index fbdd646ea13..0e0a1987c7d 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1073,12 +1073,17 @@ module Gitlab end end - def write_ref(ref_path, ref) + def write_ref(ref_path, ref, force: false) raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ') raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00") - input = "update #{ref_path}\x00#{ref}\x00\x00" - run_git!(%w[update-ref --stdin -z]) { |stdin| stdin.write(input) } + ref = "refs/heads/#{ref}" unless ref.start_with?("refs") || ref =~ /\A[a-f0-9]+\z/i + + rugged.references.create(ref_path, ref, force: force) + rescue Rugged::ReferenceError => ex + raise GitError, "could not create ref #{ref_path}: #{ex}" + rescue Rugged::OSError => ex + raise GitError, "could not create ref #{ref_path}: #{ex}" end def fetch_ref(source_repository, source_ref:, target_ref:) diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb index 65d55576ac2..9112164f22e 100644 --- a/lib/gitlab/metrics/method_call.rb +++ b/lib/gitlab/metrics/method_call.rb @@ -1,7 +1,11 @@ +# rubocop:disable Style/ClassVars + module Gitlab module Metrics # Class for tracking timing information about method calls class MethodCall + @@measurement_enabled_cache = Concurrent::AtomicBoolean.new(false) + @@measurement_enabled_cache_expires_at = Concurrent::AtomicFixnum.new(Time.now.to_i) MUTEX = Mutex.new BASE_LABELS = { module: nil, method: nil }.freeze attr_reader :real_time, :cpu_time, :call_count, :labels @@ -18,6 +22,10 @@ module Gitlab end end + def self.measurement_enabled_cache_expires_at + @@measurement_enabled_cache_expires_at + end + # name - The full name of the method (including namespace) such as # `User#sign_in`. # @@ -72,7 +80,14 @@ module Gitlab end def call_measurement_enabled? - Feature.get(:prometheus_metrics_method_instrumentation).enabled? + expires_at = @@measurement_enabled_cache_expires_at.value + if expires_at < Time.now.to_i + if @@measurement_enabled_cache_expires_at.compare_and_set(expires_at, 1.minute.from_now.to_i) + @@measurement_enabled_cache.value = Feature.get(:prometheus_metrics_method_instrumentation).enabled? + end + end + + @@measurement_enabled_cache.value end end end diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb index dc9886732b5..c3d7814551c 100644 --- a/lib/gitlab/sidekiq_config.rb +++ b/lib/gitlab/sidekiq_config.rb @@ -1,16 +1,35 @@ require 'yaml' +require 'set' module Gitlab module SidekiqConfig - def self.redis_queues - @redis_queues ||= Sidekiq::Queue.all.map(&:name) + # This method is called by `bin/sidekiq-cluster` in EE, which runs outside + # of bundler/Rails context, so we cannot use any gem or Rails methods. + def self.worker_queues(rails_path = Rails.root.to_s) + @worker_queues ||= {} + @worker_queues[rails_path] ||= YAML.load_file(File.join(rails_path, 'app/workers/all_queues.yml')) end # This method is called by `bin/sidekiq-cluster` in EE, which runs outside # of bundler/Rails context, so we cannot use any gem or Rails methods. - def self.config_queues(rails_path = Rails.root.to_s) + def self.expand_queues(queues, all_queues = self.worker_queues) + return [] if queues.empty? + + queues_set = all_queues.to_set + + queues.flat_map do |queue| + [queue, *queues_set.grep(/\A#{queue}:/)] + end + end + + def self.redis_queues + # Not memoized, because this can change during the life of the application + Sidekiq::Queue.all.map(&:name) + end + + def self.config_queues @config_queues ||= begin - config = YAML.load_file(File.join(rails_path, 'config', 'sidekiq_queues.yml')) + config = YAML.load_file(Rails.root.join('config/sidekiq_queues.yml')) config[:queues].map(&:first) end end @@ -23,14 +42,6 @@ module Gitlab @workers ||= find_workers(Rails.root.join('app', 'workers')) end - def self.default_queues - [ActionMailer::DeliveryJob.queue_name, 'default'] - end - - def self.worker_queues - @worker_queues ||= (workers.map(&:queue) + default_queues).uniq - end - def self.find_workers(root) concerns = root.join('concerns').to_s @@ -43,7 +54,7 @@ module Gitlab ns.camelize.constantize end - # Skip concerns + # Skip things that aren't workers workers.select { |w| w < Sidekiq::Worker } end end diff --git a/lib/gitlab/sidekiq_versioning.rb b/lib/gitlab/sidekiq_versioning.rb new file mode 100644 index 00000000000..9683214ec18 --- /dev/null +++ b/lib/gitlab/sidekiq_versioning.rb @@ -0,0 +1,25 @@ +module Gitlab + module SidekiqVersioning + def self.install! + Sidekiq::Manager.prepend SidekiqVersioning::Manager + + # The Sidekiq client API always adds the queue to the Sidekiq queue + # list, but mail_room and gitlab-shell do not. This is only necessary + # for monitoring. + begin + queues = SidekiqConfig.worker_queues + + if queues.any? + Sidekiq.redis do |conn| + conn.pipelined do + queues.each do |queue| + conn.sadd('queues', queue) + end + end + end + end + rescue ::Redis::BaseError, SocketError, Errno::ENOENT, Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED + end + end + end +end diff --git a/lib/gitlab/sidekiq_versioning/manager.rb b/lib/gitlab/sidekiq_versioning/manager.rb new file mode 100644 index 00000000000..308be0fdf76 --- /dev/null +++ b/lib/gitlab/sidekiq_versioning/manager.rb @@ -0,0 +1,12 @@ +module Gitlab + module SidekiqVersioning + module Manager + def initialize(options = {}) + options[:strict] = false + options[:queues] = SidekiqConfig.expand_queues(options[:queues]) + Sidekiq.logger.info "Listening on queues #{options[:queues].uniq.sort}" + super + end + end + end +end diff --git a/lib/gitlab/tcp_checker.rb b/lib/gitlab/tcp_checker.rb new file mode 100644 index 00000000000..6e24e46d0ea --- /dev/null +++ b/lib/gitlab/tcp_checker.rb @@ -0,0 +1,45 @@ +module Gitlab + class TcpChecker + attr_reader :remote_host, :remote_port, :local_host, :local_port, :error + + def initialize(remote_host, remote_port, local_host = nil, local_port = nil) + @remote_host = remote_host + @remote_port = remote_port + @local_host = local_host + @local_port = local_port + end + + def local + join_host_port(local_host, local_port) + end + + def remote + join_host_port(remote_host, remote_port) + end + + def check(timeout: 10) + Socket.tcp( + remote_host, remote_port, + local_host, local_port, + connect_timeout: timeout + ) do |sock| + @local_port, @local_host = Socket.unpack_sockaddr_in(sock.local_address) + @remote_port, @remote_host = Socket.unpack_sockaddr_in(sock.remote_address) + end + + true + rescue => err + @error = err + + false + end + + private + + def join_host_port(host, port) + host = "[#{host}]" if host.include?(':') + + "#{host}:#{port}" + end + end +end diff --git a/lib/gitlab/utils/strong_memoize.rb b/lib/gitlab/utils/strong_memoize.rb index a2ac9285b56..fe091f4611b 100644 --- a/lib/gitlab/utils/strong_memoize.rb +++ b/lib/gitlab/utils/strong_memoize.rb @@ -11,6 +11,8 @@ module Gitlab # # We could write it like: # + # include Gitlab::Utils::StrongMemoize + # # def trigger_from_token # strong_memoize(:trigger) do # Ci::Trigger.find_by_token(params[:token].to_s) @@ -18,14 +20,22 @@ module Gitlab # end # def strong_memoize(name) - ivar_name = "@#{name}" - - if instance_variable_defined?(ivar_name) - instance_variable_get(ivar_name) + if instance_variable_defined?(ivar(name)) + instance_variable_get(ivar(name)) else - instance_variable_set(ivar_name, yield) + instance_variable_set(ivar(name), yield) end end + + def clear_memoization(name) + remove_instance_variable(ivar(name)) if instance_variable_defined?(ivar(name)) + end + + private + + def ivar(name) + "@#{name}" + end end end end diff --git a/lib/gitlab/view/presenter/factory.rb b/lib/gitlab/view/presenter/factory.rb index d172d61e2c9..570f0723e39 100644 --- a/lib/gitlab/view/presenter/factory.rb +++ b/lib/gitlab/view/presenter/factory.rb @@ -16,7 +16,7 @@ module Gitlab attr_reader :subject, :attributes def presenter_class - "#{subject.class.name}Presenter".constantize + attributes.delete(:presenter_class) { "#{subject.class.name}Presenter".constantize } end end end diff --git a/lib/tasks/gitlab/tcp_check.rake b/lib/tasks/gitlab/tcp_check.rake new file mode 100644 index 00000000000..1400f57d6b9 --- /dev/null +++ b/lib/tasks/gitlab/tcp_check.rake @@ -0,0 +1,20 @@ +namespace :gitlab do + desc "GitLab | Check TCP connectivity to a specific host and port" + task :tcp_check, [:host, :port] => :environment do |_t, args| + unless args.host && args.port + puts "Please specify a host and port: `rake gitlab:tcp_check[example.com,80]`".color(:red) + exit 1 + end + + checker = Gitlab::TcpChecker.new(args.host, args.port) + + if checker.check + puts "TCP connection from #{checker.local} to #{checker.remote} succeeded".color(:green) + else + puts "TCP connection to #{checker.remote} failed: #{checker.error}".color(:red) + puts + puts 'Check that host and port are correct, and that the traffic is permitted through any firewalls.' + exit 1 + end + end +end diff --git a/qa/Dockerfile b/qa/Dockerfile index 9b6ffff7c4d..ed2ee73bea0 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.3 +FROM ruby:2.4 LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>" ENV DEBIAN_FRONTEND noninteractive diff --git a/qa/Gemfile b/qa/Gemfile index ff29824529f..4c866a3f893 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -1,8 +1,8 @@ source 'https://rubygems.org' -gem 'pry-byebug', '~> 3.4.1', platform: :mri -gem 'capybara', '~> 2.12.1' -gem 'capybara-screenshot', '~> 1.0.14' -gem 'rake', '~> 12.0.0' -gem 'rspec', '~> 3.5' -gem 'selenium-webdriver', '~> 2.53' +gem 'pry-byebug', '~> 3.5.1', platform: :mri +gem 'capybara', '~> 2.16.1' +gem 'capybara-screenshot', '~> 1.0.18' +gem 'rake', '~> 12.3.0' +gem 'rspec', '~> 3.7' +gem 'selenium-webdriver', '~> 3.8.0' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 22d12b479cb..88d5fe834a0 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -1,78 +1,72 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.5.0) - public_suffix (~> 2.0, >= 2.0.2) - byebug (9.0.6) - capybara (2.12.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + byebug (9.1.0) + capybara (2.16.1) addressable - mime-types (>= 1.16) + mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - capybara-screenshot (1.0.14) + capybara-screenshot (1.0.18) capybara (>= 1.0, < 3) launchy - childprocess (0.7.0) + childprocess (0.8.0) ffi (~> 1.0, >= 1.0.11) - coderay (1.1.1) + coderay (1.1.2) diff-lcs (1.3) ffi (1.9.18) launchy (2.4.3) addressable (~> 2.3) - method_source (0.8.2) - mime-types (3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + method_source (0.9.0) + mini_mime (1.0.0) mini_portile2 (2.3.0) nokogiri (1.8.1) mini_portile2 (~> 2.3.0) - pry (0.10.4) + pry (0.11.3) coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - pry-byebug (3.4.2) - byebug (~> 9.0) + method_source (~> 0.9.0) + pry-byebug (3.5.1) + byebug (~> 9.1) pry (~> 0.10) - public_suffix (2.0.5) - rack (2.0.1) - rack-test (0.6.3) - rack (>= 1.0) - rake (12.0.0) - rspec (3.5.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-core (3.5.4) - rspec-support (~> 3.5.0) - rspec-expectations (3.5.0) + public_suffix (3.0.1) + rack (2.0.3) + rack-test (0.8.2) + rack (>= 1.0, < 3) + rake (12.3.0) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.0) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-mocks (3.5.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-support (3.5.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.0) rubyzip (1.2.1) - selenium-webdriver (2.53.4) + selenium-webdriver (3.8.0) childprocess (~> 0.5) rubyzip (~> 1.0) - websocket (~> 1.0) - slop (3.6.0) - websocket (1.2.4) - xpath (2.0.0) + xpath (2.1.0) nokogiri (~> 1.3) PLATFORMS ruby DEPENDENCIES - capybara (~> 2.12.1) - capybara-screenshot (~> 1.0.14) - pry-byebug (~> 3.4.1) - rake (~> 12.0.0) - rspec (~> 3.5) - selenium-webdriver (~> 2.53) + capybara (~> 2.16.1) + capybara-screenshot (~> 1.0.18) + pry-byebug (~> 3.5.1) + rake (~> 12.3.0) + rspec (~> 3.7) + selenium-webdriver (~> 3.8.0) BUNDLED WITH - 1.15.4 + 1.16.0 @@ -9,6 +9,7 @@ module QA autoload :User, 'qa/runtime/user' autoload :Namespace, 'qa/runtime/namespace' autoload :Scenario, 'qa/runtime/scenario' + autoload :Browser, 'qa/runtime/browser' end ## @@ -69,7 +70,6 @@ module QA autoload :Base, 'qa/page/base' module Main - autoload :Entry, 'qa/page/main/entry' autoload :Login, 'qa/page/main/login' autoload :Menu, 'qa/page/main/menu' autoload :OAuth, 'qa/page/main/oauth' diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index f9a93ef051e..99eba02b6e3 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -10,6 +10,18 @@ module QA visit current_url end + def wait(css = '.application', time: 60) + Time.now.tap do |start| + while Time.now - start < time + break if page.has_css?(css, wait: 5) + + refresh + end + end + + yield if block_given? + end + def scroll_to(selector, text: nil) page.execute_script <<~JS var elements = Array.from(document.querySelectorAll('#{selector}')); @@ -24,6 +36,10 @@ module QA page.within(selector) { yield } if block_given? end + + def self.path + raise NotImplementedError + end end end end diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb deleted file mode 100644 index ae6484b4bfe..00000000000 --- a/qa/qa/page/main/entry.rb +++ /dev/null @@ -1,26 +0,0 @@ -module QA - module Page - module Main - class Entry < Page::Base - def visit_login_page - visit("#{Runtime::Scenario.gitlab_address}/users/sign_in") - wait_for_instance_to_be_ready - end - - private - - def wait_for_instance_to_be_ready - # This resolves cold boot / background tasks problems - # - start = Time.now - - while Time.now - start < 240 - break if page.has_css?('.application', wait: 10) - - refresh - end - end - end - end - end -end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 8b0111a78a2..f88325f408b 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -2,6 +2,10 @@ module QA module Page module Main class Login < Page::Base + def initialize + wait('.application', time: 500) + end + def sign_in_using_credentials if page.has_content?('Change your password') fill_in :user_password, with: Runtime::User.password @@ -13,6 +17,10 @@ module QA fill_in :user_password, with: Runtime::User.password click_button 'Sign in' end + + def self.path + '/users/sign_in' + end end end end diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb index 42ab9c6f675..8ffd4fdad13 100644 --- a/qa/qa/page/mattermost/login.rb +++ b/qa/qa/page/mattermost/login.rb @@ -2,10 +2,6 @@ module QA module Page module Mattermost class Login < Page::Base - def initialize - visit(Runtime::Scenario.mattermost_address + '/login') - end - def sign_in_using_oauth click_link class: 'btn btn-custom-login gitlab' @@ -13,6 +9,10 @@ module QA click_button 'Authorize' end end + + def self.path + '/login' + end end end end diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb new file mode 100644 index 00000000000..6fb37fdfc7f --- /dev/null +++ b/qa/qa/runtime/browser.rb @@ -0,0 +1,109 @@ +require 'rspec/core' +require 'capybara/rspec' +require 'capybara-screenshot/rspec' +require 'selenium-webdriver' + +module QA + module Runtime + class Browser + include QA::Scenario::Actable + + def initialize + self.class.configure! + end + + ## + # Visit a page that belongs to a GitLab instance under given address. + # + # Example: + # + # visit(:gitlab, Page::Main::Login) + # visit('http://gitlab.example/users/sign_in') + # + # In case of an address that is a symbol we will try to guess address + # based on `Runtime::Scenario#something_address`. + # + def visit(address, page, &block) + Browser::Session.new(address, page).tap do |session| + session.perform(&block) + end + end + + def self.visit(address, page, &block) + new.visit(address, page, &block) + end + + def self.configure! + return if Capybara.drivers.include?(:chrome) + + Capybara.register_driver :chrome do |app| + capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( + 'chromeOptions' => { + 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680] + } + ) + + Capybara::Selenium::Driver + .new(app, browser: :chrome, desired_capabilities: capabilities) + end + + Capybara::Screenshot.register_driver(:chrome) do |driver, path| + driver.browser.save_screenshot(path) + end + + Capybara.configure do |config| + config.default_driver = :chrome + config.javascript_driver = :chrome + config.default_max_wait_time = 10 + # https://github.com/mattheworiordan/capybara-screenshot/issues/164 + config.save_path = 'tmp' + end + end + + class Session + include Capybara::DSL + + def initialize(instance, page = nil) + @instance = instance + @address = host + page&.path + end + + def host + if @instance.is_a?(Symbol) + Runtime::Scenario.send("#{@instance}_address") + else + @instance.to_s + end + end + + def perform(&block) + visit(@address) + + yield if block_given? + rescue + raise if block.nil? + + # RSpec examples will take care of screenshots on their own + # + unless block.binding.receiver.is_a?(RSpec::Core::ExampleGroup) + screenshot_and_save_page + end + + raise + ensure + clear! if block_given? + end + + ## + # Selenium allows to reset session cookies for current domain only. + # + # See gitlab-org/gitlab-qa#102 + # + def clear! + visit(@address) + reset_session! + end + end + end + end +end diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb index b9d924651a0..ae099fd911e 100644 --- a/qa/qa/scenario/entrypoint.rb +++ b/qa/qa/scenario/entrypoint.rb @@ -8,7 +8,6 @@ module QA include Bootable def perform(address, *files) - Specs::Config.act { configure_capybara! } Runtime::Scenario.define(:gitlab_address, address) ## diff --git a/qa/qa/scenario/gitlab/admin/hashed_storage.rb b/qa/qa/scenario/gitlab/admin/hashed_storage.rb index ac2cd549829..44604c6bc66 100644 --- a/qa/qa/scenario/gitlab/admin/hashed_storage.rb +++ b/qa/qa/scenario/gitlab/admin/hashed_storage.rb @@ -6,7 +6,6 @@ module QA def perform(*traits) raise ArgumentError unless traits.include?(:enabled) - Page::Main::Entry.act { visit_login_page } Page::Main::Login.act { sign_in_using_credentials } Page::Main::Menu.act { go_to_admin_area } Page::Admin::Menu.act { go_to_settings } diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb deleted file mode 100644 index bce7923e52d..00000000000 --- a/qa/qa/specs/config.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'rspec/core' -require 'capybara/rspec' -require 'capybara-screenshot/rspec' -require 'selenium-webdriver' - -# rubocop:disable Metrics/MethodLength -# rubocop:disable Metrics/LineLength - -module QA - module Specs - class Config < Scenario::Template - include Scenario::Actable - - def perform - configure_rspec! - configure_capybara! - end - - def configure_rspec! - RSpec.configure do |config| - config.expect_with :rspec do |expectations| - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - - config.mock_with :rspec do |mocks| - mocks.verify_partial_doubles = true - end - - config.order = :random - Kernel.srand config.seed - config.formatter = :documentation - config.color = true - end - end - - def configure_capybara! - Capybara.register_driver :chrome do |app| - capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( - 'chromeOptions' => { - 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680] - } - ) - - Capybara::Selenium::Driver - .new(app, browser: :chrome, desired_capabilities: capabilities) - end - - Capybara::Screenshot.register_driver(:chrome) do |driver, path| - driver.browser.save_screenshot(path) - end - - Capybara.configure do |config| - config.default_driver = :chrome - config.javascript_driver = :chrome - config.default_max_wait_time = 10 - - # https://github.com/mattheworiordan/capybara-screenshot/issues/164 - config.save_path = 'tmp' - end - end - end - end -end diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb index b155708c387..9eaa2b772e6 100644 --- a/qa/qa/specs/features/login/standard_spec.rb +++ b/qa/qa/specs/features/login/standard_spec.rb @@ -1,7 +1,7 @@ module QA - feature 'standard root login', :core do + feature 'standard user login', :core do scenario 'user logs in using credentials' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } # TODO, since `Signed in successfully` message was removed diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb index 853a9a6a4f4..b3dbe44bf6e 100644 --- a/qa/qa/specs/features/mattermost/group_create_spec.rb +++ b/qa/qa/specs/features/mattermost/group_create_spec.rb @@ -1,7 +1,7 @@ module QA feature 'create a new group', :mattermost do scenario 'creating a group with a mattermost team' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } Page::Main::Menu.act { go_to_groups } diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb index 1fde3f89a99..637bbdd643a 100644 --- a/qa/qa/specs/features/mattermost/login_spec.rb +++ b/qa/qa/specs/features/mattermost/login_spec.rb @@ -1,24 +1,17 @@ module QA feature 'logging in to Mattermost', :mattermost do scenario 'can use gitlab oauth' do - Page::Main::Entry.act { visit_login_page } - Page::Main::Login.act { sign_in_using_credentials } - Page::Mattermost::Login.act { sign_in_using_oauth } + Runtime::Browser.visit(:gitlab, Page::Main::Login) do + Page::Main::Login.act { sign_in_using_credentials } - Page::Mattermost::Main.perform do |page| - expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) - end - end + Runtime::Browser.visit(:mattermost, Page::Mattermost::Login) do + Page::Mattermost::Login.act { sign_in_using_oauth } - ## - # TODO, temporary workaround for gitlab-org/gitlab-qa#102. - # - after do - visit Runtime::Scenario.mattermost_address - reset_session! - - visit Runtime::Scenario.gitlab_address - reset_session! + Page::Mattermost::Main.perform do |page| + expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) + end + end + end end end end diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb index aba0c2b4c14..0b3accb848d 100644 --- a/qa/qa/specs/features/project/create_spec.rb +++ b/qa/qa/specs/features/project/create_spec.rb @@ -1,7 +1,7 @@ module QA feature 'create a new project', :core do scenario 'user creates a new project' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } Scenario::Gitlab::Project::Create.perform do |project| diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb index 5cc3b3b9c1b..c5c24622657 100644 --- a/qa/qa/specs/features/repository/clone_spec.rb +++ b/qa/qa/specs/features/repository/clone_spec.rb @@ -9,7 +9,7 @@ module QA end before do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } Scenario::Gitlab::Project::Create.perform do |scenario| diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index 5b930b9818a..ef29dfa2d2f 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -2,7 +2,7 @@ module QA feature 'push code to repository', :core do context 'with regular account over http' do scenario 'user pushes code to the repository' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } Scenario::Gitlab::Project::Create.perform do |scenario| diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index f98b8f88e9a..3f7b75df986 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -17,7 +17,7 @@ module QA tags.to_a.each { |tag| args.push(['-t', tag.to_s]) } args.push(files) - Specs::Config.perform + Runtime::Browser.configure! RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status| abort if status.nonzero? diff --git a/rubocop/cop/include_sidekiq_worker.rb b/rubocop/cop/include_sidekiq_worker.rb new file mode 100644 index 00000000000..4a6332286a2 --- /dev/null +++ b/rubocop/cop/include_sidekiq_worker.rb @@ -0,0 +1,29 @@ +require_relative '../spec_helpers' + +module RuboCop + module Cop + # Cop that makes sure workers include `ApplicationWorker`, not `Sidekiq::Worker`. + class IncludeSidekiqWorker < RuboCop::Cop::Cop + include SpecHelpers + + MSG = 'Include `ApplicationWorker`, not `Sidekiq::Worker`.'.freeze + + def_node_matcher :includes_sidekiq_worker?, <<~PATTERN + (send nil :include (const (const nil :Sidekiq) :Worker)) + PATTERN + + def on_send(node) + return if in_spec?(node) + return unless includes_sidekiq_worker?(node) + + add_offense(node.arguments.first, :expression) + end + + def autocorrect(node) + lambda do |corrector| + corrector.replace(node.source_range, 'ApplicationWorker') + end + end + end + end +end diff --git a/rubocop/cop/sidekiq_options_queue.rb b/rubocop/cop/sidekiq_options_queue.rb new file mode 100644 index 00000000000..43b35ba0214 --- /dev/null +++ b/rubocop/cop/sidekiq_options_queue.rb @@ -0,0 +1,27 @@ +require_relative '../spec_helpers' + +module RuboCop + module Cop + # Cop that prevents manually setting a queue in Sidekiq workers. + class SidekiqOptionsQueue < RuboCop::Cop::Cop + include SpecHelpers + + MSG = 'Do not manually set a queue; `ApplicationWorker` sets one automatically.'.freeze + + def_node_matcher :sidekiq_options?, <<~PATTERN + (send nil :sidekiq_options $...) + PATTERN + + def on_send(node) + return if in_spec?(node) + return unless sidekiq_options?(node) + + node.arguments.first.each_node(:pair) do |pair| + key_name = pair.key.children[0] + + add_offense(pair, :expression) if key_name == :queue + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index eb52be3d731..3e3b4c8349a 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -3,10 +3,12 @@ require_relative 'cop/active_record_serialize' require_relative 'cop/custom_error_class' require_relative 'cop/gem_fetcher' require_relative 'cop/in_batches' +require_relative 'cop/include_sidekiq_worker' require_relative 'cop/line_break_after_guard_clauses' require_relative 'cop/polymorphic_associations' require_relative 'cop/project_path_helper' require_relative 'cop/redirect_with_status' +require_relative 'cop/sidekiq_options_queue' require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb index f4f12a095fc..4e2d8e8969e 100644 --- a/spec/factories/commits.rb +++ b/spec/factories/commits.rb @@ -2,15 +2,28 @@ require_relative '../support/repo_helpers' FactoryGirl.define do factory :commit do - git_commit RepoHelpers.sample_commit + transient do + author nil + end + + git_commit do + commit = RepoHelpers.sample_commit + + if author + commit.author_email = author.email + commit.author_name = author.name + end + + commit + end project initialize_with do new(git_commit, project) end - after(:build) do |commit| - allow(commit).to receive(:author).and_return build(:author) + after(:build) do |commit, evaluator| + allow(commit).to receive(:author).and_return(evaluator.author || build(:author)) end trait :without_author do diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index c870910c8ea..77dcdf89f37 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -197,7 +197,7 @@ describe 'Commits' do commits = project.repository.commits(branch_name) commits.each do |commit| - expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}") + expect(page).to have_content("authored #{commit.authored_date.strftime("%b %d, %Y")}") end end diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 6f916078b1a..94133c62b5c 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -13,7 +13,7 @@ feature 'Dashboard Todos' do end it 'shows "All done" message' do - expect(page).to have_content 'Todos let you see what you should do next.' + expect(page).to have_content 'Todos let you see what you should do next' end end diff --git a/spec/features/groups/labels/user_sees_links_to_issuables.rb b/spec/features/groups/labels/user_sees_links_to_issuables.rb new file mode 100644 index 00000000000..5d6290d2109 --- /dev/null +++ b/spec/features/groups/labels/user_sees_links_to_issuables.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +feature 'Groups > Labels > User sees links to issuables' do + set(:group) { create(:group, :public) } + + before do + create(:group_label, group: group, title: 'bug') + visit group_labels_path(group) + end + + scenario 'shows links to MRs and issues' do + expect(page).to have_link('view merge requests') + expect(page).to have_link('view open issues') + end +end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index c60883911f7..0848857ed1e 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -25,7 +25,7 @@ describe 'Profile account page', :js do fill_in 'password', with: '12345678' - page.within '.popup-dialog' do + page.within '.modal' do click_button 'Delete account' end @@ -38,7 +38,7 @@ describe 'Profile account page', :js do fill_in 'password', with: 'testing123' - page.within '.popup-dialog' do + page.within '.modal' do click_button 'Delete account' end diff --git a/spec/features/projects/labels/user_sees_links_to_issuables.rb b/spec/features/projects/labels/user_sees_links_to_issuables.rb new file mode 100644 index 00000000000..aa56fd7f74e --- /dev/null +++ b/spec/features/projects/labels/user_sees_links_to_issuables.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +feature 'Projects > Labels > User sees links to issuables' do + set(:user) { create(:user) } + + before do + label # creates the label + project.add_developer(user) + sign_in user + visit project_labels_path(project) + end + + context 'with a project label' do + let(:label) { create(:label, project: project, title: 'bug') } + + context 'when merge requests and issues are enabled for the project' do + let(:project) { create(:project, :public) } + + scenario 'shows links to MRs and issues' do + expect(page).to have_link('view merge requests') + expect(page).to have_link('view open issues') + end + end + + context 'when issues are disabled for the project' do + let(:project) { create(:project, :public, issues_access_level: ProjectFeature::DISABLED) } + + scenario 'shows links to MRs but not to issues' do + expect(page).to have_link('view merge requests') + expect(page).not_to have_link('view open issues') + end + end + + context 'when merge requests are disabled for the project' do + let(:project) { create(:project, :public, merge_requests_access_level: ProjectFeature::DISABLED) } + + scenario 'shows links to issues but not to MRs' do + expect(page).not_to have_link('view merge requests') + expect(page).to have_link('view open issues') + end + end + end + + context 'with a group label' do + set(:group) { create(:group) } + let(:label) { create(:group_label, group: group, title: 'bug') } + + context 'when merge requests and issues are enabled for the project' do + let(:project) { create(:project, :public, namespace: group) } + + scenario 'shows links to MRs and issues' do + expect(page).to have_link('view merge requests') + expect(page).to have_link('view open issues') + end + end + + context 'when issues are disabled for the project' do + let(:project) { create(:project, :public, namespace: group, issues_access_level: ProjectFeature::DISABLED) } + + scenario 'shows links to MRs and issues' do + expect(page).to have_link('view merge requests') + expect(page).to have_link('view open issues') + end + end + + context 'when merge requests are disabled for the project' do + let(:project) { create(:project, :public, namespace: group, merge_requests_access_level: ProjectFeature::DISABLED) } + + scenario 'shows links to MRs and issues' do + expect(page).to have_link('view merge requests') + expect(page).to have_link('view open issues') + end + end + end +end diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index 156293289dd..8f06328962e 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -20,7 +20,7 @@ feature 'Multi-file editor new directory', :js do click_link('New directory') - page.within('.popup-dialog') do + page.within('.modal') do find('.form-control').set('foldername') click_button('Create directory') diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index 8fb8476e631..bdebc12ef47 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -20,7 +20,7 @@ feature 'Multi-file editor new file', :js do click_link('New file') - page.within('.popup-dialog') do + page.within('.modal') do find('.form-control').set('filename') click_button('Create file') diff --git a/spec/fixtures/api/schemas/contributor.json b/spec/fixtures/api/schemas/contributor.json new file mode 100644 index 00000000000..e88470a2363 --- /dev/null +++ b/spec/fixtures/api/schemas/contributor.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "required" : [ + "name", + "email", + "commits", + "additions", + "deletions" + ], + "properties" : { + "name": { "type": "string" }, + "email": { "type": "string" }, + "commits": { "type": "integer" }, + "additions": { "type": "integer" }, + "deletions": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/contributors.json b/spec/fixtures/api/schemas/contributors.json new file mode 100644 index 00000000000..a9f1d1ea64f --- /dev/null +++ b/spec/fixtures/api/schemas/contributors.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "contributor.json" } +} diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 4ac4302adfd..0286d36952c 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -1,6 +1,69 @@ require 'spec_helper' describe LabelsHelper do + describe '#show_label_issuables_link?' do + shared_examples 'a valid response to show_label_issuables_link?' do |issuables_type, when_enabled = true, when_disabled = false| + let(:context_project) { project } + + context "when asking for a #{issuables_type} link" do + subject { show_label_issuables_link?(label, issuables_type, project: context_project) } + + context "when #{issuables_type} are enabled for the project" do + let(:project) { create(:project, "#{issuables_type}_access_level": ProjectFeature::ENABLED) } + + it { is_expected.to be(when_enabled) } + end + + context "when #{issuables_type} are disabled for the project" do + let(:project) { create(:project, :public, "#{issuables_type}_access_level": ProjectFeature::DISABLED) } + + it { is_expected.to be(when_disabled) } + end + end + end + + context 'with a project label' do + let(:label) { create(:label, project: project, title: 'bug') } + + context 'when asking for an issue link' do + it_behaves_like 'a valid response to show_label_issuables_link?', :issues + end + + context 'when asking for a merge requests link' do + it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests + end + end + + context 'with a group label' do + set(:group) { create(:group) } + let(:label) { create(:group_label, group: group, title: 'bug') } + + context 'when asking for an issue link' do + context 'in the context of a project' do + it_behaves_like 'a valid response to show_label_issuables_link?', :issues, true, true + end + + context 'in the context of a group' do + let(:context_project) { nil } + + it_behaves_like 'a valid response to show_label_issuables_link?', :issues, true, true + end + end + + context 'when asking for a merge requests link' do + context 'in the context of a project' do + it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests, true, true + end + + context 'in the context of a group' do + let(:context_project) { nil } + + it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests, true, true + end + end + end + end + describe 'link_to_label' do let(:project) { create(:project) } let(:label) { create(:label, project: project) } diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index 33186cf50d5..45ffbeb27a4 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -1,14 +1,6 @@ require 'spec_helper' describe MembersHelper do - describe '#action_member_permission' do - let(:project_member) { build(:project_member) } - let(:group_member) { build(:group_member) } - - it { expect(action_member_permission(:admin, project_member)).to eq :admin_project_member } - it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member } - end - describe '#remove_member_message' do let(:requester) { create(:user) } let(:project) { create(:project, :public, :access_requestable) } diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js index e8c5f721423..fc9be14df8f 100644 --- a/spec/javascripts/activities_spec.js +++ b/spec/javascripts/activities_spec.js @@ -2,7 +2,7 @@ import 'vendor/jquery.endless-scroll'; import '~/pager'; -import '~/activities'; +import Activities from '~/activities'; (() => { window.gon || (window.gon = {}); @@ -35,7 +35,7 @@ import '~/activities'; describe('Activities', () => { beforeEach(() => { loadFixtures(fixtureTemplate); - new gl.Activities(); + new Activities(); }); for (let i = 0; i < filters.length; i += 1) { diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index 0f7bf9ec712..2e5b65f5610 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -1,110 +1,108 @@ import * as datetimeUtility from '~/lib/utils/datetime_utility'; -(() => { - describe('Date time utils', () => { - describe('timeFor', () => { - it('returns `past due` when in past', () => { - const date = new Date(); - date.setFullYear(date.getFullYear() - 1); - - expect( - gl.utils.timeFor(date), - ).toBe('Past due'); - }); - - it('returns remaining time when in the future', () => { - const date = new Date(); - date.setFullYear(date.getFullYear() + 1); - - // Add a day to prevent a transient error. If date is even 1 second - // short of a full year, timeFor will return '11 months remaining' - date.setDate(date.getDate() + 1); - - expect( - gl.utils.timeFor(date), - ).toBe('1 year remaining'); - }); +describe('Date time utils', () => { + describe('timeFor', () => { + it('returns `past due` when in past', () => { + const date = new Date(); + date.setFullYear(date.getFullYear() - 1); + + expect( + datetimeUtility.timeFor(date), + ).toBe('Past due'); }); - describe('get day name', () => { - it('should return Sunday', () => { - const day = gl.utils.getDayName(new Date('07/17/2016')); - expect(day).toBe('Sunday'); - }); - - it('should return Monday', () => { - const day = gl.utils.getDayName(new Date('07/18/2016')); - expect(day).toBe('Monday'); - }); - - it('should return Tuesday', () => { - const day = gl.utils.getDayName(new Date('07/19/2016')); - expect(day).toBe('Tuesday'); - }); - - it('should return Wednesday', () => { - const day = gl.utils.getDayName(new Date('07/20/2016')); - expect(day).toBe('Wednesday'); - }); - - it('should return Thursday', () => { - const day = gl.utils.getDayName(new Date('07/21/2016')); - expect(day).toBe('Thursday'); - }); - - it('should return Friday', () => { - const day = gl.utils.getDayName(new Date('07/22/2016')); - expect(day).toBe('Friday'); - }); - - it('should return Saturday', () => { - const day = gl.utils.getDayName(new Date('07/23/2016')); - expect(day).toBe('Saturday'); - }); - }); + it('returns remaining time when in the future', () => { + const date = new Date(); + date.setFullYear(date.getFullYear() + 1); + + // Add a day to prevent a transient error. If date is even 1 second + // short of a full year, timeFor will return '11 months remaining' + date.setDate(date.getDate() + 1); - describe('get day difference', () => { - it('should return 7', () => { - const firstDay = new Date('07/01/2016'); - const secondDay = new Date('07/08/2016'); - const difference = gl.utils.getDayDifference(firstDay, secondDay); - expect(difference).toBe(7); - }); - - it('should return 31', () => { - const firstDay = new Date('07/01/2016'); - const secondDay = new Date('08/01/2016'); - const difference = gl.utils.getDayDifference(firstDay, secondDay); - expect(difference).toBe(31); - }); - - it('should return 365', () => { - const firstDay = new Date('07/02/2015'); - const secondDay = new Date('07/01/2016'); - const difference = gl.utils.getDayDifference(firstDay, secondDay); - expect(difference).toBe(365); - }); + expect( + datetimeUtility.timeFor(date), + ).toBe('1 year remaining'); }); }); - describe('timeIntervalInWords', () => { - it('should return string with number of minutes and seconds', () => { - expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds'); - expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second'); - expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); - expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); + describe('get day name', () => { + it('should return Sunday', () => { + const day = datetimeUtility.getDayName(new Date('07/17/2016')); + expect(day).toBe('Sunday'); + }); + + it('should return Monday', () => { + const day = datetimeUtility.getDayName(new Date('07/18/2016')); + expect(day).toBe('Monday'); + }); + + it('should return Tuesday', () => { + const day = datetimeUtility.getDayName(new Date('07/19/2016')); + expect(day).toBe('Tuesday'); + }); + + it('should return Wednesday', () => { + const day = datetimeUtility.getDayName(new Date('07/20/2016')); + expect(day).toBe('Wednesday'); + }); + + it('should return Thursday', () => { + const day = datetimeUtility.getDayName(new Date('07/21/2016')); + expect(day).toBe('Thursday'); + }); + + it('should return Friday', () => { + const day = datetimeUtility.getDayName(new Date('07/22/2016')); + expect(day).toBe('Friday'); + }); + + it('should return Saturday', () => { + const day = datetimeUtility.getDayName(new Date('07/23/2016')); + expect(day).toBe('Saturday'); }); }); - describe('dateInWords', () => { - const date = new Date('07/01/2016'); + describe('get day difference', () => { + it('should return 7', () => { + const firstDay = new Date('07/01/2016'); + const secondDay = new Date('07/08/2016'); + const difference = datetimeUtility.getDayDifference(firstDay, secondDay); + expect(difference).toBe(7); + }); - it('should return date in words', () => { - expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016'); + it('should return 31', () => { + const firstDay = new Date('07/01/2016'); + const secondDay = new Date('08/01/2016'); + const difference = datetimeUtility.getDayDifference(firstDay, secondDay); + expect(difference).toBe(31); }); - it('should return abbreviated month name', () => { - expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016'); + it('should return 365', () => { + const firstDay = new Date('07/02/2015'); + const secondDay = new Date('07/01/2016'); + const difference = datetimeUtility.getDayDifference(firstDay, secondDay); + expect(difference).toBe(365); }); }); -})(); +}); + +describe('timeIntervalInWords', () => { + it('should return string with number of minutes and seconds', () => { + expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds'); + expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second'); + expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); + expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); + }); +}); + +describe('dateInWords', () => { + const date = new Date('07/01/2016'); + + it('should return date in words', () => { + expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016'); + }); + + it('should return abbreviated month name', () => { + expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016'); + }); +}); diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index 5b64cbb2dfc..2f28c5bbf01 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import DeployKeysStore from '~/deploy_keys/store'; import key from '~/deploy_keys/components/key.vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; describe('Deploy keys key', () => { let vm; @@ -37,7 +38,7 @@ describe('Deploy keys key', () => { it('renders human friendly formatted created date', () => { expect( vm.$el.querySelector('.key-created-at').textContent.trim(), - ).toBe(`created ${gl.utils.getTimeago().format(deployKey.created_at)}`); + ).toBe(`created ${getTimeago().format(deployKey.created_at)}`); }); it('shows edit button', () => { diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js index 2ce1a749a96..7a5c1da4d1d 100644 --- a/spec/javascripts/groups/components/item_actions_spec.js +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -36,27 +36,27 @@ describe('ItemActionsComponent', () => { describe('methods', () => { describe('onLeaveGroup', () => { - it('should change `dialogStatus` prop to `true` which shows confirmation dialog', () => { - expect(vm.dialogStatus).toBeFalsy(); + it('should change `modalStatus` prop to `true` which shows confirmation dialog', () => { + expect(vm.modalStatus).toBeFalsy(); vm.onLeaveGroup(); - expect(vm.dialogStatus).toBeTruthy(); + expect(vm.modalStatus).toBeTruthy(); }); }); describe('leaveGroup', () => { - it('should change `dialogStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { + it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { spyOn(eventHub, '$emit'); - vm.dialogStatus = true; + vm.modalStatus = true; vm.leaveGroup(true); - expect(vm.dialogStatus).toBeFalsy(); + expect(vm.modalStatus).toBeFalsy(); expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); }); - it('should change `dialogStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { + it('should change `modalStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { spyOn(eventHub, '$emit'); - vm.dialogStatus = true; + vm.modalStatus = true; vm.leaveGroup(false); - expect(vm.dialogStatus).toBeFalsy(); + expect(vm.modalStatus).toBeFalsy(); expect(eventHub.$emit).not.toHaveBeenCalled(); }); }); @@ -99,9 +99,9 @@ describe('ItemActionsComponent', () => { newVm.$destroy(); }); - it('should show modal dialog when `dialogStatus` is set to `true`', () => { - vm.dialogStatus = true; - const modalDialogEl = vm.$el.querySelector('.modal.popup-dialog'); + it('should show modal dialog when `modalStatus` is set to `true`', () => { + vm.modalStatus = true; + const modalDialogEl = vm.$el.querySelector('.modal'); expect(modalDialogEl).toBeDefined(); expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 729c3c29f22..7159148f8fa 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -272,10 +272,10 @@ describe('Issuable output', () => { }); }); - it('opens recaptcha dialog if update rejected as spam', (done) => { + it('opens recaptcha modal if update rejected as spam', (done) => { function mockScriptSrc() { const recaptchaChild = vm.$children - .find(child => child.$options._componentTag === 'recaptcha-dialog'); // eslint-disable-line no-underscore-dangle + .find(child => child.$options._componentTag === 'recaptcha-modal'); // eslint-disable-line no-underscore-dangle recaptchaChild.scriptSrc = '//scriptsrc'; } @@ -302,7 +302,7 @@ describe('Issuable output', () => { .then(promise) .then(() => setTimeoutPromise()) .then(() => { - modal = vm.$el.querySelector('.js-recaptcha-dialog'); + modal = vm.$el.querySelector('.js-recaptcha-modal'); expect(modal.style.display).not.toEqual('none'); expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html'); diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index 2e000a1063f..0da25bdca9c 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -54,7 +54,7 @@ describe('Description component', () => { it('opens recaptcha dialog if update rejected as spam', (done) => { let modal; const recaptchaChild = vm.$children - .find(child => child.$options._componentTag === 'recaptcha-dialog'); // eslint-disable-line no-underscore-dangle + .find(child => child.$options._componentTag === 'recaptcha-modal'); // eslint-disable-line no-underscore-dangle recaptchaChild.scriptSrc = '//scriptsrc'; @@ -64,7 +64,7 @@ describe('Description component', () => { vm.$nextTick() .then(() => { - modal = vm.$el.querySelector('.js-recaptcha-dialog'); + modal = vm.$el.querySelector('.js-recaptcha-modal'); expect(modal.style.display).not.toEqual('none'); expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html'); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 3636aac79a0..2cd2e63b15d 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -55,7 +55,7 @@ describe('Issue', function() { } function findElements(isIssueInitiallyOpen) { - $boxClosed = $('div.status-box-closed'); + $boxClosed = $('div.status-box-issue-closed'); expect($boxClosed).toExist(); expect($boxClosed).toHaveText('Closed'); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 2612c5fd7bc..e09b8dc7fc5 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -222,7 +222,6 @@ import '~/notes'; notes.note_ids = []; notes.updatedNotesTrackingMap = {}; - spyOn(gl.utils, 'localTimeAgo'); spyOn(Notes, 'isNewNote').and.callThrough(); spyOn(Notes, 'isUpdatedNote').and.callThrough(); spyOn(Notes, 'animateAppendNote').and.callThrough(); @@ -349,7 +348,6 @@ import '~/notes'; ]); notes.note_ids = []; - spyOn(gl.utils, 'localTimeAgo'); spyOn(Notes, 'isNewNote'); spyOn(Notes, 'animateAppendNote'); Notes.isNewNote.and.returnValue(true); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 5505f983d71..72790eb215a 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -41,7 +41,7 @@ import '~/right_sidebar'; loadFixtures(fixtureName); this.sidebar = new Sidebar; $aside = $('.right-sidebar'); - $page = $('.page-with-sidebar'); + $page = $('.layout-page'); $icon = $aside.find('i'); $toggle = $aside.find('.js-sidebar-toggle'); return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 1e4c2c9faad..206f95abc1a 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */ import '~/gl_dropdown'; -import '~/search_autocomplete'; +import SearchAutocomplete from '~/search_autocomplete'; import '~/lib/utils/common_utils'; import * as urlUtils from '~/lib/utils/url_utility'; @@ -128,7 +128,7 @@ import * as urlUtils from '~/lib/utils/url_utility'; window.gon.current_user_id = userId; window.gon.current_username = userName; - return widget = new gl.SearchAutocomplete; + return widget = new SearchAutocomplete(); }); afterEach(function() { diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index 946f98379ce..763a15e710b 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,44 +1,42 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */ -import '~/syntax_highlight'; +import syntaxHighlight from '~/syntax_highlight'; -(function() { - describe('Syntax Highlighter', function() { - var stubUserColorScheme; - stubUserColorScheme = function(value) { - if (window.gon == null) { - window.gon = {}; - } - return window.gon.user_color_scheme = value; - }; - describe('on a js-syntax-highlight element', function() { - beforeEach(function() { - return setFixtures('<div class="js-syntax-highlight"></div>'); - }); - return it('applies syntax highlighting', function() { - stubUserColorScheme('monokai'); - $('.js-syntax-highlight').syntaxHighlight(); - return expect($('.js-syntax-highlight')).toHaveClass('monokai'); - }); +describe('Syntax Highlighter', function() { + var stubUserColorScheme; + stubUserColorScheme = function(value) { + if (window.gon == null) { + window.gon = {}; + } + return window.gon.user_color_scheme = value; + }; + describe('on a js-syntax-highlight element', function() { + beforeEach(function() { + return setFixtures('<div class="js-syntax-highlight"></div>'); }); - return describe('on a parent element', function() { - beforeEach(function() { - return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); - }); - it('applies highlighting to all applicable children', function() { - stubUserColorScheme('monokai'); - $('.parent').syntaxHighlight(); - expect($('.parent, .foo')).not.toHaveClass('monokai'); - return expect($('.monokai').length).toBe(2); - }); - return it('prevents an infinite loop when no matches exist', function() { - var highlight; - setFixtures('<div></div>'); - highlight = function() { - return $('div').syntaxHighlight(); - }; - return expect(highlight).not.toThrow(); - }); + return it('applies syntax highlighting', function() { + stubUserColorScheme('monokai'); + syntaxHighlight($('.js-syntax-highlight')); + return expect($('.js-syntax-highlight')).toHaveClass('monokai'); }); }); -}).call(window); + return describe('on a parent element', function() { + beforeEach(function() { + return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); + }); + it('applies highlighting to all applicable children', function() { + stubUserColorScheme('monokai'); + syntaxHighlight($('.parent')); + expect($('.parent, .foo')).not.toHaveClass('monokai'); + return expect($('.monokai').length).toBe(2); + }); + return it('prevents an infinite loop when no matches exist', function() { + var highlight; + setFixtures('<div></div>'); + highlight = function() { + return syntaxHighlight($('div')); + }; + return expect(highlight).not.toThrow(); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js index d5b8947c86f..db7d083065b 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import * as urlUtils from '~/lib/utils/url_utility'; import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; +import { getTimeago } from '~/lib/utils/datetime_utility'; const deploymentMockData = [ { @@ -49,7 +50,7 @@ describe('MRWidgetDeployment', () => { describe('formatDate', () => { it('should work', () => { - const readable = gl.utils.getTimeago().format(deployment.deployed_at); + const readable = getTimeago().format(deployment.deployed_at); expect(vm.formatDate(deployment.deployed_at)).toEqual(readable); }); }); diff --git a/spec/javascripts/vue_shared/components/popup_dialog_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js index 5c1d2a196f4..721f4044659 100644 --- a/spec/javascripts/vue_shared/components/popup_dialog_spec.js +++ b/spec/javascripts/vue_shared/components/modal_spec.js @@ -1,11 +1,11 @@ import Vue from 'vue'; -import PopupDialog from '~/vue_shared/components/popup_dialog.vue'; +import modal from '~/vue_shared/components/modal.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; -describe('PopupDialog', () => { +describe('Modal', () => { it('does not render a primary button if no primaryButtonLabel', () => { - const popupDialog = Vue.extend(PopupDialog); - const vm = mountComponent(popupDialog); + const modalComponent = Vue.extend(modal); + const vm = mountComponent(modalComponent); expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); }); diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js index b4c1f70ed1e..b4fb568f1d4 100644 --- a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js +++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import '~/lib/utils/datetime_utility'; +import { formatDate, getTimeago } from '~/lib/utils/datetime_utility'; describe('Time ago with tooltip component', () => { let TimeagoTooltip; @@ -24,10 +24,10 @@ describe('Time ago with tooltip component', () => { expect(vm.$el.tagName).toEqual('TIME'); expect( vm.$el.getAttribute('data-original-title'), - ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z')); + ).toEqual(formatDate('2017-05-08T14:57:39.781Z')); expect(vm.$el.getAttribute('data-placement')).toEqual('top'); - const timeago = gl.utils.getTimeago(); + const timeago = getTimeago(); expect(vm.$el.textContent.trim()).toEqual(timeago.format('2017-05-08T14:57:39.781Z')); }); diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb index 79d2c071446..e1c4f9cfea7 100644 --- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb +++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb @@ -2,19 +2,20 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migration, schema: 20170929131201 do let(:migration) { described_class.new } + let(:projects) { table(:projects) } - let(:base1) { create(:project) } - let(:base1_fork1) { create(:project) } - let(:base1_fork2) { create(:project) } + let(:base1) { projects.create } + let(:base1_fork1) { projects.create } + let(:base1_fork2) { projects.create } - let(:base2) { create(:project) } - let(:base2_fork1) { create(:project) } - let(:base2_fork2) { create(:project) } + let(:base2) { projects.create } + let(:base2_fork1) { projects.create } + let(:base2_fork2) { projects.create } - let(:fork_of_fork) { create(:project) } - let(:fork_of_fork2) { create(:project) } - let(:second_level_fork) { create(:project) } - let(:third_level_fork) { create(:project) } + let(:fork_of_fork) { projects.create } + let(:fork_of_fork2) { projects.create } + let(:second_level_fork) { projects.create } + let(:third_level_fork) { projects.create } let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) } let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) } @@ -97,7 +98,7 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat end it 'does not miss members for forks of forks for which the root was deleted' do - forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: create(:project).id) + forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: projects.create.id) base1.destroy expect(migration.missing_members?(7, 10)).to be_falsy @@ -105,8 +106,8 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat context 'with more forks' do before do - forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) - forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) + forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: projects.create.id) + forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: projects.create.id) end it 'only processes a single batch of links at a time' do diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index cb52d971047..3998ca940a4 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -225,7 +225,8 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati let(:user_class) { table(:users) } let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) } let(:namespace) { create(:namespace, owner: author) } - let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) } + let(:projects) { table(:projects) } + let(:project) { projects.create(namespace_id: namespace.id, creator_id: author.id) } # We can not rely on FactoryGirl as the state of Event may change in ways that # the background migration does not expect, hence we use the Event class of diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb index e52baf8dde7..8582af96199 100644 --- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do let(:migration) { described_class.new } - let(:base1) { create(:project) } + let(:projects) { table(:projects) } + let(:base1) { projects.create } - let(:base2) { create(:project) } - let(:base2_fork1) { create(:project) } + let(:base2) { projects.create } + let(:base2_fork1) { projects.create } let!(:forked_project_links) { table(:forked_project_links) } let!(:fork_networks) { table(:fork_networks) } @@ -18,10 +19,10 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch # A normal fork link forked_project_links.create(id: 1, forked_from_project_id: base1.id, - forked_to_project_id: create(:project).id) + forked_to_project_id: projects.create.id) forked_project_links.create(id: 2, forked_from_project_id: base1.id, - forked_to_project_id: create(:project).id) + forked_to_project_id: projects.create.id) forked_project_links.create(id: 3, forked_from_project_id: base2.id, forked_to_project_id: base2_fork1.id) @@ -29,10 +30,10 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch # create a fork of a fork forked_project_links.create(id: 4, forked_from_project_id: base2_fork1.id, - forked_to_project_id: create(:project).id) + forked_to_project_id: projects.create.id) forked_project_links.create(id: 5, - forked_from_project_id: create(:project).id, - forked_to_project_id: create(:project).id) + forked_from_project_id: projects.create.id, + forked_to_project_id: projects.create.id) # Stub out the calls to the other migrations allow(BackgroundMigrationWorker).to receive(:perform_in) @@ -63,7 +64,7 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch end it 'creates a fork network for the fork of which the source was deleted' do - fork = create(:project) + fork = projects.create forked_project_links.create(id: 6, forked_from_project_id: 99999, forked_to_project_id: fork.id) migration.perform(5, 8) diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb index b80df6956b0..be45c00dfe6 100644 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb @@ -182,13 +182,22 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do end context 'for a pre-Markdown Note attachment file path' do - class Note < ActiveRecord::Base - has_many :uploads, as: :model, dependent: :destroy + let(:model) { create(:note, :with_attachment) } + let!(:expected_upload_attrs) { Upload.where(model_type: 'Note', model_id: model.id).first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } + + before do + Upload.where(model_type: 'Note', model_id: model.id).delete_all end - let(:model) { create(:note, :with_attachment) } + # Can't use the shared example because Note doesn't have an `uploads` association + it 'creates an Upload record' do + expect do + subject.perform(1, untracked_files_for_uploads.last.id) + end.to change { Upload.where(model_type: 'Note', model_id: model.id).count }.from(0).to(1) - it_behaves_like 'non_markdown_file' + expect(Upload.where(model_type: 'Note', model_id: model.id).first.attributes).to include(expected_upload_attrs) + end end context 'for a user avatar file path' do diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb index 5341addf911..78767d06462 100644 --- a/spec/lib/gitlab/metrics/method_call_spec.rb +++ b/spec/lib/gitlab/metrics/method_call_spec.rb @@ -20,9 +20,39 @@ describe Gitlab::Metrics::MethodCall do context 'prometheus instrumentation is enabled' do before do + allow(Feature.get(:prometheus_metrics_method_instrumentation)).to receive(:enabled?).and_call_original + described_class.measurement_enabled_cache_expires_at.value = Time.now.to_i - 1 Feature.get(:prometheus_metrics_method_instrumentation).enable end + around do |example| + Timecop.freeze do + example.run + end + end + + it 'caches subsequent invocations of feature check' do + 10.times do + method_call.measure { 'foo' } + end + + expect(Feature.get(:prometheus_metrics_method_instrumentation)).to have_received(:enabled?).once + end + + it 'expires feature check cache after 1 minute' do + method_call.measure { 'foo' } + + Timecop.travel(1.minute.from_now) do + method_call.measure { 'foo' } + end + + Timecop.travel(1.minute.from_now + 1.second) do + method_call.measure { 'foo' } + end + + expect(Feature.get(:prometheus_metrics_method_instrumentation)).to have_received(:enabled?).twice + end + it 'observes the performance of the supplied block' do expect(described_class.call_duration_histogram) .to receive(:observe) @@ -34,6 +64,8 @@ describe Gitlab::Metrics::MethodCall do context 'prometheus instrumentation is disabled' do before do + described_class.measurement_enabled_cache_expires_at.value = Time.now.to_i - 1 + Feature.get(:prometheus_metrics_method_instrumentation).disable end diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb index 09f95be2213..0c66d764851 100644 --- a/spec/lib/gitlab/sidekiq_config_spec.rb +++ b/spec/lib/gitlab/sidekiq_config_spec.rb @@ -16,9 +16,30 @@ describe Gitlab::SidekiqConfig do expect(queues).to include('post_receive') expect(queues).to include('merge') - expect(queues).to include('cronjob') + expect(queues).to include('cronjob:stuck_import_jobs') expect(queues).to include('mailers') expect(queues).to include('default') end end + + describe '.expand_queues' do + it 'expands queue namespaces to concrete queue names' do + queues = described_class.expand_queues(%w[cronjob]) + + expect(queues).to include('cronjob:stuck_import_jobs') + expect(queues).to include('cronjob:stuck_merge_jobs') + end + + it 'lets concrete queue names pass through' do + queues = described_class.expand_queues(%w[post_receive]) + + expect(queues).to include('post_receive') + end + + it 'lets unknown queues pass through' do + queues = described_class.expand_queues(%w[unknown]) + + expect(queues).to include('unknown') + end + end end diff --git a/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb b/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb new file mode 100644 index 00000000000..7debf70a16f --- /dev/null +++ b/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::SidekiqVersioning::Manager do + before do + Sidekiq::Manager.prepend described_class + end + + describe '#initialize' do + it 'listens on all expanded queues' do + manager = Sidekiq::Manager.new(queues: %w[post_receive repository_fork cronjob unknown]) + + queues = manager.options[:queues] + + expect(queues).to include('post_receive') + expect(queues).to include('repository_fork') + expect(queues).to include('cronjob') + expect(queues).to include('cronjob:stuck_import_jobs') + expect(queues).to include('cronjob:stuck_merge_jobs') + expect(queues).to include('unknown') + end + end +end diff --git a/spec/lib/gitlab/sidekiq_versioning_spec.rb b/spec/lib/gitlab/sidekiq_versioning_spec.rb new file mode 100644 index 00000000000..fa6d42e730d --- /dev/null +++ b/spec/lib/gitlab/sidekiq_versioning_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Gitlab::SidekiqVersioning, :sidekiq, :redis do + let(:foo_worker) do + Class.new do + def self.name + 'FooWorker' + end + + include ApplicationWorker + end + end + + let(:bar_worker) do + Class.new do + def self.name + 'BarWorker' + end + + include ApplicationWorker + end + end + + before do + allow(Gitlab::SidekiqConfig).to receive(:workers).and_return([foo_worker, bar_worker]) + allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return([foo_worker.queue, bar_worker.queue]) + end + + describe '.install!' do + it 'prepends SidekiqVersioning::Manager into Sidekiq::Manager' do + described_class.install! + + expect(Sidekiq::Manager).to include(Gitlab::SidekiqVersioning::Manager) + end + + it 'registers all versionless and versioned queues with Redis' do + described_class.install! + + queues = Sidekiq::Queue.all.map(&:name) + expect(queues).to include('foo') + expect(queues).to include('bar') + end + end +end diff --git a/spec/lib/gitlab/tcp_checker_spec.rb b/spec/lib/gitlab/tcp_checker_spec.rb new file mode 100644 index 00000000000..4acf0334496 --- /dev/null +++ b/spec/lib/gitlab/tcp_checker_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Gitlab::TcpChecker do + before do + @server = TCPServer.new('localhost', 0) + _, @port, _, @ip = @server.addr + end + + after do + @server.close + end + + subject(:checker) { described_class.new(@ip, @port) } + + describe '#check' do + subject { checker.check } + + it 'can connect to an open port' do + is_expected.to be_truthy + + expect(checker.error).to be_nil + end + + it 'fails to connect to a closed port' do + @server.close + + is_expected.to be_falsy + + expect(checker.error).to be_a(Errno::ECONNREFUSED) + end + end +end diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb index 4a104ab6d97..473f8100771 100644 --- a/spec/lib/gitlab/utils/strong_memoize_spec.rb +++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb @@ -49,4 +49,16 @@ describe Gitlab::Utils::StrongMemoize do end end end + + describe '#clear_memoization' do + let(:value) { 'mepmep' } + + it 'removes the instance variable' do + object.method_name + + object.clear_memoization(:method_name) + + expect(object.instance_variable_defined?(:@method_name)).to be(false) + end + end end diff --git a/spec/lib/gitlab/view/presenter/factory_spec.rb b/spec/lib/gitlab/view/presenter/factory_spec.rb index 70d2e22b48f..6120bafb2e3 100644 --- a/spec/lib/gitlab/view/presenter/factory_spec.rb +++ b/spec/lib/gitlab/view/presenter/factory_spec.rb @@ -27,5 +27,13 @@ describe Gitlab::View::Presenter::Factory do expect(presenter).to be_a(Ci::BuildPresenter) end + + it 'uses the presenter_class if given on #initialize' do + MyCustomPresenter = Class.new(described_class) + + presenter = described_class.new(build, presenter_class: MyCustomPresenter).fabricate! + + expect(presenter).to be_a(MyCustomPresenter) + end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index e1d71a9573b..4d0a3942996 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -342,6 +342,46 @@ describe Notify do end end + context 'for issue notes' do + let(:host) { Gitlab.config.gitlab.host } + + context 'in discussion' do + set(:first_note) { create(:discussion_note_on_issue) } + set(:second_note) { create(:discussion_note_on_issue, in_reply_to: first_note) } + set(:third_note) { create(:discussion_note_on_issue, in_reply_to: second_note) } + + subject { described_class.note_issue_email(recipient.id, third_note.id) } + + it 'has In-Reply-To header pointing to previous note in discussion' do + expect(subject.header['In-Reply-To'].message_ids).to eq(["note_#{second_note.id}@#{host}"]) + end + + it 'has References header including the notes and issue of the discussion' do + expect(subject.header['References'].message_ids).to include("issue_#{first_note.noteable.id}@#{host}", + "note_#{first_note.id}@#{host}", + "note_#{second_note.id}@#{host}") + end + + it 'has X-GitLab-Discussion-ID header' do + expect(subject.header['X-GitLab-Discussion-ID'].value).to eq(third_note.discussion.id) + end + end + + context 'individual issue comments' do + set(:note) { create(:note_on_issue) } + + subject { described_class.note_issue_email(recipient.id, note.id) } + + it 'has In-Reply-To header pointing to the issue' do + expect(subject.header['In-Reply-To'].message_ids).to eq(["issue_#{note.noteable.id}@#{host}"]) + end + + it 'has References header including the notes and issue of the discussion' do + expect(subject.header['References'].message_ids).to include("issue_#{note.noteable.id}@#{host}") + end + end + end + context 'for snippet notes' do let(:project_snippet) { create(:project_snippet, project: project) } let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) } diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb index 05f281fffff..57ee2adaaff 100644 --- a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb +++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb @@ -2,9 +2,10 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb') describe MigrateGcpClustersToNewClustersArchitectures, :migration do - let(:project) { create(:project) } + let(:projects) { table(:projects) } + let(:project) { projects.create } let(:user) { create(:user) } - let(:service) { create(:kubernetes_service, project: project) } + let(:service) { create(:kubernetes_service, project_id: project.id) } context 'when cluster is being created' do let(:project_id) { project.id } @@ -56,8 +57,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do expect(cluster.provider_type).to eq('gcp') expect(cluster.platform_type).to eq('kubernetes') - expect(cluster.project).to eq(project) - expect(project.clusters).to include(cluster) + expect(cluster.project_ids).to include(project.id) expect(cluster.provider_gcp.cluster).to eq(cluster) expect(cluster.provider_gcp.status).to eq(status) @@ -133,8 +133,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do expect(cluster.provider_type).to eq('gcp') expect(cluster.platform_type).to eq('kubernetes') - expect(cluster.project).to eq(project) - expect(project.clusters).to include(cluster) + expect(cluster.project_ids).to include(project.id) expect(cluster.provider_gcp.cluster).to eq(cluster) expect(cluster.provider_gcp.status).to eq(status) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index c5e23532aa5..871e8b47650 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1861,9 +1861,9 @@ describe Ci::Build do describe 'state transition: any => [:running]' do shared_examples 'validation is active' do context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } - it { expect { job.run! }.to raise_error(Ci::Build::MissingDependenciesError) } + it { expect { job.run! }.not_to raise_error(Ci::Build::MissingDependenciesError) } end context 'when artifacts of depended job has been expired' do @@ -1885,11 +1885,10 @@ describe Ci::Build do shared_examples 'validation is not active' do context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } it { expect { job.run! }.not_to raise_error } end - context 'when artifacts of depended job has been expired' do let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) } diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index 129dfa07f15..3c7f578975b 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -102,6 +102,26 @@ describe CacheMarkdownField do it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) } end + context 'when a markdown field is set repeatedly to an empty string' do + it do + expect(thing).to receive(:refresh_markdown_cache).once + thing.foo = '' + thing.save + thing.foo = '' + thing.save + end + end + + context 'when a markdown field is set repeatedly to a string which renders as empty html' do + it do + expect(thing).to receive(:refresh_markdown_cache).once + thing.foo = '[//]: # (This is also a comment.)' + thing.save + thing.foo = '[//]: # (This is also a comment.)' + thing.save + end + end + context 'a non-markdown field changed' do before do thing.bar = 'OK' diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 30a5a3bbff7..bb63abd167b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -124,6 +124,7 @@ describe MergeRequest do context 'when the target branch does not exist' do before do project.repository.rm_branch(subject.author, subject.target_branch) + subject.clear_memoized_shas end it 'returns nil' do @@ -600,30 +601,30 @@ describe MergeRequest do end describe '#can_remove_source_branch?' do - let(:user) { create(:user) } - let(:user2) { create(:user) } + set(:user) { create(:user) } + set(:merge_request) { create(:merge_request, :simple) } - before do - subject.source_project.team << [user, :master] + subject { merge_request } - subject.source_branch = "feature" - subject.target_branch = "master" - subject.save! + before do + subject.source_project.add_master(user) end it "can't be removed when its a protected branch" do allow(ProtectedBranch).to receive(:protected?).and_return(true) + expect(subject.can_remove_source_branch?(user)).to be_falsey end it "can't remove a root ref" do - subject.source_branch = "master" - subject.target_branch = "feature" + subject.update(source_branch: 'master', target_branch: 'feature') expect(subject.can_remove_source_branch?(user)).to be_falsey end it "is unable to remove the source branch for a project the user cannot push to" do + user2 = create(:user) + expect(subject.can_remove_source_branch?(user2)).to be_falsey end @@ -634,6 +635,7 @@ describe MergeRequest do end it "cannot be removed if the last commit is not also the head of the source branch" do + subject.clear_memoized_shas subject.source_branch = "lfs" expect(subject.can_remove_source_branch?(user)).to be_falsey @@ -733,7 +735,7 @@ describe MergeRequest do before do project.repository.raw_repository.delete_branch(subject.target_branch) - subject.reload + subject.clear_memoized_shas end it 'does not crash' do @@ -1404,6 +1406,16 @@ describe MergeRequest do subject.reload_diff end + + context 'when using the after_update hook to update' do + context 'when the branches are updated' do + it 'uses the new heads to generate the diff' do + expect { subject.update!(source_branch: subject.target_branch, target_branch: subject.source_branch) } + .to change { subject.merge_request_diff.start_commit_sha } + .and change { subject.merge_request_diff.head_commit_sha } + end + end + end end describe '#update_diff_discussion_positions' do @@ -1468,6 +1480,7 @@ describe MergeRequest do context 'when the target branch does not exist' do before do subject.project.repository.rm_branch(subject.author, subject.target_branch) + subject.clear_memoized_shas end it 'returns nil' do @@ -1855,4 +1868,20 @@ describe MergeRequest do it_behaves_like 'throttled touch' do subject { create(:merge_request, updated_at: 1.hour.ago) } end + + context 'state machine transitions' do + describe '#unlock_mr' do + subject { create(:merge_request, state: 'locked', merge_jid: 123) } + + it 'updates merge request head pipeline and sets merge_jid to nil' do + pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha) + + subject.unlock_mr + + subject.reload + expect(subject.head_pipeline).to eq(pipeline) + expect(subject.merge_jid).to be_nil + end + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index e1a0c55b6a6..cefbf60b28c 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -756,6 +756,28 @@ describe Note do end end + describe '#references' do + context 'when part of a discussion' do + it 'references all earlier notes in the discussion' do + first_note = create(:discussion_note_on_issue) + second_note = create(:discussion_note_on_issue, in_reply_to: first_note) + third_note = create(:discussion_note_on_issue, in_reply_to: second_note) + create(:discussion_note_on_issue, in_reply_to: third_note) + + expect(third_note.references).to eq([first_note.noteable, first_note, second_note]) + end + end + + context 'when not part of a discussion' do + subject { create(:note) } + let(:note) { create(:note, in_reply_to: subject) } + + it 'returns the noteable' do + expect(note.references).to eq([note.noteable]) + end + end + end + describe 'expiring ETag cache' do let(:note) { build(:note_on_issue) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f4699fd243d..dd9e8498519 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1863,10 +1863,11 @@ describe Project do project.change_head(project.default_branch) end - it 'creates the new reference with rugged' do - expect(project.repository.rugged.references).to receive(:create).with('HEAD', + it 'creates the new reference' do + expect(project.repository.raw_repository).to receive(:write_ref).with('HEAD', "refs/heads/#{project.default_branch}", force: true) + project.change_head(project.default_branch) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 358bc3dfb94..f0661b0a972 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -59,12 +59,18 @@ describe Repository do end describe 'tags_sorted_by' do - context 'name' do - subject { repository.tags_sorted_by('name').map(&:name) } + context 'name_desc' do + subject { repository.tags_sorted_by('name_desc').map(&:name) } it { is_expected.to eq(['v1.1.0', 'v1.0.0']) } end + context 'name_asc' do + subject { repository.tags_sorted_by('name_asc').map(&:name) } + + it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } + end + context 'updated' do let(:tag_a) { repository.find_tag('v1.0.0') } let(:tag_b) { repository.find_tag('v1.1.0') } @@ -1916,6 +1922,23 @@ describe Repository do File.delete(path) end + + it "attempting to call keep_around when exists a lock does not fail" do + ref = repository.send(:keep_around_ref_name, sample_commit.id) + path = File.join(repository.path, ref) + lock_path = "#{path}.lock" + + FileUtils.mkdir_p(File.dirname(path)) + File.open(lock_path, 'w') { |f| f.write('') } + + begin + expect { repository.keep_around(sample_commit.id) }.not_to raise_error(Gitlab::Git::Repository::GitError) + + expect(File.exist?(lock_path)).to be_falsey + ensure + File.delete(path) + end + end end describe '#update_ref' do @@ -2343,4 +2366,111 @@ describe Repository do end end end + + describe '#contributors' do + let(:author_a) { build(:author, email: 'tiagonbotelho@hotmail.com', name: 'tiagonbotelho') } + let(:author_b) { build(:author, email: 'gitlab@winniehell.de', name: 'Winnie') } + let(:author_c) { build(:author, email: 'douwe@gitlab.com', name: 'Douwe Maan') } + let(:stubbed_commits) do + [build(:commit, author: author_a), + build(:commit, author: author_a), + build(:commit, author: author_b), + build(:commit, author: author_c), + build(:commit, author: author_c), + build(:commit, author: author_c)] + end + let(:order_by) { nil } + let(:sort) { nil } + + before do + allow(repository).to receive(:commits).with(nil, limit: 2000, offset: 0, skip_merges: true).and_return(stubbed_commits) + end + + subject { repository.contributors(order_by: order_by, sort: sort) } + + def expect_contributors(*contributors) + expect(subject.map(&:email)).to eq(contributors.map(&:email)) + end + + it 'returns the array of Gitlab::Contributor for the repository' do + expect_contributors(author_a, author_b, author_c) + end + + context 'order_by email' do + let(:order_by) { 'email' } + + context 'asc' do + let(:sort) { 'asc' } + + it 'returns all the contributors ordered by email asc case insensitive' do + expect_contributors(author_c, author_b, author_a) + end + end + + context 'desc' do + let(:sort) { 'desc' } + + it 'returns all the contributors ordered by email desc case insensitive' do + expect_contributors(author_a, author_b, author_c) + end + end + end + + context 'order_by name' do + let(:order_by) { 'name' } + + context 'asc' do + let(:sort) { 'asc' } + + it 'returns all the contributors ordered by name asc case insensitive' do + expect_contributors(author_c, author_a, author_b) + end + end + + context 'desc' do + let(:sort) { 'desc' } + + it 'returns all the contributors ordered by name desc case insensitive' do + expect_contributors(author_b, author_a, author_c) + end + end + end + + context 'order_by commits' do + let(:order_by) { 'commits' } + + context 'asc' do + let(:sort) { 'asc' } + + it 'returns all the contributors ordered by commits asc' do + expect_contributors(author_b, author_a, author_c) + end + end + + context 'desc' do + let(:sort) { 'desc' } + + it 'returns all the contributors ordered by commits desc' do + expect_contributors(author_c, author_a, author_b) + end + end + end + + context 'invalid ordering' do + let(:order_by) { 'unknown' } + + it 'returns the contributors unsorted' do + expect_contributors(author_a, author_b, author_c) + end + end + + context 'invalid sorting' do + let(:order_by) { 'name' } + let(:sort) { 'unknown' } + + it 'returns the contributors unsorted' do + expect_contributors(author_a, author_b, author_c) + end + end + end end diff --git a/spec/presenters/group_member_presenter_spec.rb b/spec/presenters/group_member_presenter_spec.rb new file mode 100644 index 00000000000..c00e41725d9 --- /dev/null +++ b/spec/presenters/group_member_presenter_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +describe GroupMemberPresenter do + let(:user) { double(:user) } + let(:group) { double(:group) } + let(:group_member) { double(:group_member, source: group) } + let(:presenter) { described_class.new(group_member, current_user: user) } + + describe '#can_resend_invite?' do + context 'when group_member is invited' do + before do + expect(group_member).to receive(:invite?).and_return(true) + end + + context 'and user can admin_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(true) + end + + it { expect(presenter.can_resend_invite?).to eq(true) } + end + + context 'and user cannot admin_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(false) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + end + + context 'when group_member is not invited' do + before do + expect(group_member).to receive(:invite?).and_return(false) + end + + context 'and user can admin_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(true) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + + context 'and user cannot admin_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(false) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + end + end + + describe '#can_update?' do + context 'when user can update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true) + end + + it { expect(presenter.can_update?).to eq(true) } + end + + context 'when user cannot update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false) + allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(false) + end + + it { expect(presenter.can_update?).to eq(false) } + end + end + + describe '#can_remove?' do + context 'when user can destroy_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :destroy_group_member, presenter).and_return(true) + end + + it { expect(presenter.can_remove?).to eq(true) } + end + + context 'when user cannot destroy_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :destroy_group_member, presenter).and_return(false) + end + + it { expect(presenter.can_remove?).to eq(false) } + end + end + + describe '#can_approve?' do + context 'when group_member has request an invite' do + before do + expect(group_member).to receive(:request?).and_return(true) + end + + context 'when user can update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true) + end + + it { expect(presenter.can_approve?).to eq(true) } + end + + context 'when user cannot update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false) + allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(false) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + end + + context 'when group_member did not request an invite' do + before do + expect(group_member).to receive(:request?).and_return(false) + end + + context 'when user can update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + + context 'when user cannot update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + end + end +end diff --git a/spec/presenters/project_member_presenter_spec.rb b/spec/presenters/project_member_presenter_spec.rb new file mode 100644 index 00000000000..83db5c56cdf --- /dev/null +++ b/spec/presenters/project_member_presenter_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +describe ProjectMemberPresenter do + let(:user) { double(:user) } + let(:project) { double(:project) } + let(:project_member) { double(:project_member, source: project) } + let(:presenter) { described_class.new(project_member, current_user: user) } + + describe '#can_resend_invite?' do + context 'when project_member is invited' do + before do + expect(project_member).to receive(:invite?).and_return(true) + end + + context 'and user can admin_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(true) + end + + it { expect(presenter.can_resend_invite?).to eq(true) } + end + + context 'and user cannot admin_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(false) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + end + + context 'when project_member is not invited' do + before do + expect(project_member).to receive(:invite?).and_return(false) + end + + context 'and user can admin_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(true) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + + context 'and user cannot admin_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(false) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + end + end + + describe '#can_update?' do + context 'when user can update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true) + end + + it { expect(presenter.can_update?).to eq(true) } + end + + context 'when user cannot update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false) + allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(false) + end + + it { expect(presenter.can_update?).to eq(false) } + end + end + + describe '#can_remove?' do + context 'when user can destroy_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :destroy_project_member, presenter).and_return(true) + end + + it { expect(presenter.can_remove?).to eq(true) } + end + + context 'when user cannot destroy_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :destroy_project_member, presenter).and_return(false) + end + + it { expect(presenter.can_remove?).to eq(false) } + end + end + + describe '#can_approve?' do + context 'when project_member has request an invite' do + before do + expect(project_member).to receive(:request?).and_return(true) + end + + context 'and user can update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true) + end + + it { expect(presenter.can_approve?).to eq(true) } + end + + context 'and user cannot update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false) + allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(false) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + end + + context 'when project_member did not request an invite' do + before do + expect(project_member).to receive(:request?).and_return(false) + end + + context 'and user can update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + + context 'and user cannot update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + end + end +end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 9f2ff3b5af6..741800ff61d 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -378,6 +378,28 @@ describe API::Repositories do expect(first_contributor['additions']).to eq(0) expect(first_contributor['deletions']).to eq(0) end + + context 'using sorting' do + context 'by commits desc' do + it 'returns the repository contribuors sorted by commits desc' do + get api(route, current_user), { order_by: 'commits', sort: 'desc' } + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('contributors') + expect(json_response.first['commits']).to be > json_response.last['commits'] + end + end + + context 'by name desc' do + it 'returns the repository contribuors sorted by name asc case insensitive' do + get api(route, current_user), { order_by: 'name', sort: 'asc' } + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('contributors') + expect(json_response.first['name'].downcase).to be < json_response.last['name'].downcase + end + end + end end context 'when unauthenticated', 'and project is public' do diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 0bf7863bdc8..e2b19ad59f9 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -16,6 +16,44 @@ describe API::Tags do describe 'GET /projects/:id/repository/tags' do let(:route) { "/projects/#{project_id}/repository/tags" } + context 'sorting' do + let(:current_user) { user } + + it 'sorts by descending order by default' do + get api(route, current_user) + + desc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date } + desc_order_tags.reverse!.map! { |tag| tag.dereferenced_target.id } + + expect(json_response.map { |tag| tag['commit']['id'] }).to eq(desc_order_tags) + end + + it 'sorts by ascending order if specified' do + get api("#{route}?sort=asc", current_user) + + asc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date } + asc_order_tags.map! { |tag| tag.dereferenced_target.id } + + expect(json_response.map { |tag| tag['commit']['id'] }).to eq(asc_order_tags) + end + + it 'sorts by name in descending order when requested' do + get api("#{route}?order_by=name", current_user) + + ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort.reverse + + expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name) + end + + it 'sorts by name in ascending order when requested' do + get api("#{route}?order_by=name&sort=asc", current_user) + + ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort + + expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name) + end + end + shared_examples_for 'repository tags' do it 'returns the repository tags' do get api(route, current_user) diff --git a/spec/rubocop/cop/include_sidekiq_worker_spec.rb b/spec/rubocop/cop/include_sidekiq_worker_spec.rb new file mode 100644 index 00000000000..7f406535dda --- /dev/null +++ b/spec/rubocop/cop/include_sidekiq_worker_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/include_sidekiq_worker' + +describe RuboCop::Cop::IncludeSidekiqWorker do + include CopHelper + + subject(:cop) { described_class.new } + + context 'when `Sidekiq::Worker` is included' do + let(:source) { 'include Sidekiq::Worker' } + let(:correct_source) { 'include ApplicationWorker' } + + it 'registers an offense ' do + inspect_source(cop, source) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['Sidekiq::Worker']) + end + end + + it 'autocorrects to the right version' do + autocorrected = autocorrect_source(cop, source) + + expect(autocorrected).to eq(correct_source) + end + end +end diff --git a/spec/rubocop/cop/sidekiq_options_queue_spec.rb b/spec/rubocop/cop/sidekiq_options_queue_spec.rb new file mode 100644 index 00000000000..a31de381631 --- /dev/null +++ b/spec/rubocop/cop/sidekiq_options_queue_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/sidekiq_options_queue' + +describe RuboCop::Cop::SidekiqOptionsQueue do + include CopHelper + + subject(:cop) { described_class.new } + + it 'registers an offense when `sidekiq_options` is used with the `queue` option' do + inspect_source(cop, 'sidekiq_options queue: "some_queue"') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['queue: "some_queue"']) + end + end + + it 'does not register an offense when `sidekiq_options` is used with another option' do + inspect_source(cop, 'sidekiq_options retry: false') + + expect(cop.offenses).to be_empty + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 41ce81e0651..267258b33a8 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -64,6 +64,18 @@ describe Ci::CreatePipelineService do create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project) end + context 'when related merge request is already merged' do + let!(:merged_merge_request) do + create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project, state: 'merged') + end + + it 'does not schedule update head pipeline job' do + expect(UpdateHeadPipelineForMergeRequestWorker).not_to receive(:perform_async).with(merged_merge_request.id) + + execute_service + end + end + context 'when the head pipeline sha equals merge request sha' do it 'updates head pipeline of each merge request' do merge_request_1 @@ -77,13 +89,13 @@ describe Ci::CreatePipelineService do end context 'when the head pipeline sha does not equal merge request sha' do - it 'raises the ArgumentError error from worker and does not update the head piepeline of MRs' do + it 'does not update the head piepeline of MRs' do merge_request_1 merge_request_2 allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(true) - expect { execute_service(after: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }.to raise_error(ArgumentError) + expect { execute_service(after: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }.not_to raise_error last_pipeline = Ci::Pipeline.last diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 22fb7ed7215..de8a9ce12ff 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -287,9 +287,9 @@ module Ci shared_examples 'validation is active' do context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } - it_behaves_like 'not pick' + it { expect(subject).to eq(pending_job) } end context 'when artifacts of depended job has been expired' do @@ -309,7 +309,7 @@ module Ci end context 'when job object is staled' do - let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) } before do allow_any_instance_of(Ci::Build).to receive(:drop!) @@ -324,11 +324,10 @@ module Ci shared_examples 'validation is not active' do context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } it { expect(subject).to eq(pending_job) } end - context 'when artifacts of depended job has been expired' do let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) } diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index a7ab389b357..623b182b205 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -100,5 +100,17 @@ describe MergeRequests::CreateFromIssueService do expect(result[:merge_request].target_branch).to eq(project.default_branch) end + + it 'executes quick actions if the build service sets them in the description' do + allow(service).to receive(:merge_request).and_wrap_original do |m, *args| + m.call(*args).tap do |merge_request| + merge_request.description = "/assign #{user.to_reference}" + end + end + + result = service.execute + + expect(result[:merge_request].assignee).to eq(user) + end end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index f86f1ac2443..c38ddf4612b 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' describe MergeRequests::MergeService do - let(:user) { create(:user) } - let(:user2) { create(:user) } + set(:user) { create(:user) } + set(:user2) { create(:user) } let(:merge_request) { create(:merge_request, :simple, author: user2, assignee: user2) } let(:project) { merge_request.project } before do - project.team << [user, :master] - project.team << [user2, :developer] + project.add_master(user) + project.add_developer(user2) end describe '#execute' do diff --git a/spec/support/batch_loader.rb b/spec/support/batch_loader.rb new file mode 100644 index 00000000000..bb790e660a6 --- /dev/null +++ b/spec/support/batch_loader.rb @@ -0,0 +1,5 @@ +RSpec.configure do |config| + config.after do + BatchLoader::Executor.clear_current + end +end diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb index 0145563e0ed..901d77178bc 100644 --- a/spec/workers/concerns/application_worker_spec.rb +++ b/spec/workers/concerns/application_worker_spec.rb @@ -17,6 +17,14 @@ describe ApplicationWorker do end end + describe '.queue_namespace' do + it 'sets the queue name based on the class name' do + worker.queue_namespace :some_namespace + + expect(worker.queue).to eq('some_namespace:foo_bar_dummy') + end + end + describe '.queue' do it 'returns the queue name' do worker.sidekiq_options queue: :some_queue diff --git a/spec/workers/concerns/cluster_queue_spec.rb b/spec/workers/concerns/cluster_queue_spec.rb index 5049886b55c..4118b9aa194 100644 --- a/spec/workers/concerns/cluster_queue_spec.rb +++ b/spec/workers/concerns/cluster_queue_spec.rb @@ -14,6 +14,6 @@ describe ClusterQueue do it 'sets a default pipelines queue automatically' do expect(worker.sidekiq_options['queue']) - .to eq :gcp_cluster + .to eq 'gcp_cluster:dummy' end end diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb index 3ae1c5f54d8..c042a52f41f 100644 --- a/spec/workers/concerns/cronjob_queue_spec.rb +++ b/spec/workers/concerns/cronjob_queue_spec.rb @@ -13,7 +13,7 @@ describe CronjobQueue do end it 'sets the queue name of a worker' do - expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob') + expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob:dummy') end it 'disables retrying of failed jobs' do diff --git a/spec/workers/concerns/gitlab/github_import/queue_spec.rb b/spec/workers/concerns/gitlab/github_import/queue_spec.rb index 9c69ee32da1..a96f583aff7 100644 --- a/spec/workers/concerns/gitlab/github_import/queue_spec.rb +++ b/spec/workers/concerns/gitlab/github_import/queue_spec.rb @@ -11,6 +11,6 @@ describe Gitlab::GithubImport::Queue do include Gitlab::GithubImport::Queue end - expect(worker.sidekiq_options['queue']).to eq('github_importer') + expect(worker.sidekiq_options['queue']).to eq('github_importer:dummy') end end diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb index dd911760948..a312b307fce 100644 --- a/spec/workers/concerns/pipeline_queue_spec.rb +++ b/spec/workers/concerns/pipeline_queue_spec.rb @@ -14,15 +14,6 @@ describe PipelineQueue do it 'sets a default pipelines queue automatically' do expect(worker.sidekiq_options['queue']) - .to eq 'pipeline_default' - end - - describe '.enqueue_in' do - it 'sets a custom sidekiq queue with prefix and group' do - worker.enqueue_in(group: :processing) - - expect(worker.sidekiq_options['queue']) - .to eq 'pipeline_processing' - end + .to eq 'pipeline_default:dummy' end end diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb index fdbbfcc90a5..d2eeecfc9a8 100644 --- a/spec/workers/concerns/repository_check_queue_spec.rb +++ b/spec/workers/concerns/repository_check_queue_spec.rb @@ -13,7 +13,7 @@ describe RepositoryCheckQueue do end it 'sets the queue name of a worker' do - expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check') + expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check:dummy') end it 'disables retrying of failed jobs' do diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 7ee0a51a263..9e3b99b3502 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -1,21 +1,36 @@ require 'spec_helper' describe 'Every Sidekiq worker' do - it 'includes ApplicationWorker' do - expect(Gitlab::SidekiqConfig.workers).to all(include(ApplicationWorker)) - end - it 'does not use the default queue' do expect(Gitlab::SidekiqConfig.workers.map(&:queue)).not_to include('default') end it 'uses the cronjob queue when the worker runs as a cronjob' do - expect(Gitlab::SidekiqConfig.cron_workers.map(&:queue)).to all(eq('cronjob')) + expect(Gitlab::SidekiqConfig.cron_workers.map(&:queue)).to all(start_with('cronjob:')) + end + + it 'has its queue in app/workers/all_queues.yml', :aggregate_failures do + file_worker_queues = Gitlab::SidekiqConfig.worker_queues.to_set + + worker_queues = Gitlab::SidekiqConfig.workers.map(&:queue).to_set + worker_queues << ActionMailer::DeliveryJob.queue_name + worker_queues << 'default' + + missing_from_file = worker_queues - file_worker_queues + expect(missing_from_file).to be_empty, "expected #{missing_from_file.to_a.inspect} to be in app/workers/all_queues.yml" + + unncessarily_in_file = file_worker_queues - worker_queues + expect(unncessarily_in_file).to be_empty, "expected #{unncessarily_in_file.to_a.inspect} not to be in app/workers/all_queues.yml" end - it 'defines the queue in the Sidekiq configuration file' do - config_queue_names = Gitlab::SidekiqConfig.config_queues.to_set + it 'has its queue or namespace in config/sidekiq_queues.yml', :aggregate_failures do + config_queues = Gitlab::SidekiqConfig.config_queues.to_set + + Gitlab::SidekiqConfig.workers.each do |worker| + queue = worker.queue + queue_namespace = queue.split(':').first - expect(Gitlab::SidekiqConfig.worker_queues).to all(be_in(config_queue_names)) + expect(config_queues).to include(queue).or(include(queue_namespace)) + end end end diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb index f8b55e873df..c2c2a5f9121 100644 --- a/spec/workers/stuck_merge_jobs_worker_spec.rb +++ b/spec/workers/stuck_merge_jobs_worker_spec.rb @@ -14,7 +14,6 @@ describe StuckMergeJobsWorker do mr_with_sha.reload mr_without_sha.reload - expect(mr_with_sha).to be_merged expect(mr_without_sha).to be_opened expect(mr_with_sha.merge_jid).to be_present @@ -24,10 +23,13 @@ describe StuckMergeJobsWorker do it 'updates merge request to opened when locked but has not been merged' do allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123)) merge_request = create(:merge_request, :locked, merge_jid: '123', state: :locked) + pipeline = create(:ci_empty_pipeline, project: merge_request.project, ref: merge_request.source_branch, sha: merge_request.source_branch_sha) worker.perform - expect(merge_request.reload).to be_opened + merge_request.reload + expect(merge_request).to be_opened + expect(merge_request.head_pipeline).to eq(pipeline) end it 'logs updated stuck merge job ids' do diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb index 522e1566271..9adde5fc21a 100644 --- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb +++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb @@ -22,7 +22,7 @@ describe UpdateHeadPipelineForMergeRequestWorker do end it 'does not update head_pipeline_id' do - expect { subject.perform(merge_request.id) }.to raise_error(ArgumentError) + expect { subject.perform(merge_request.id) }.not_to raise_error expect(merge_request.reload.head_pipeline_id).to eq(nil) end diff --git a/vendor/Dockerfile/CONTRIBUTING.md b/vendor/Dockerfile/CONTRIBUTING.md index 0878db6dd9e..3e98f2e7b5b 100644 --- a/vendor/Dockerfile/CONTRIBUTING.md +++ b/vendor/Dockerfile/CONTRIBUTING.md @@ -1,22 +1,15 @@ -The canonical repository for `Dockerfile` templates is -https://gitlab.com/gitlab-org/Dockerfile. +## Developer Certificate of Origin + License -GitLab only mirrors the templates. Please submit your merge requests to -https://gitlab.com/gitlab-org/Dockerfile. +By contributing to GitLab B.V., You accept and agree to the following terms and +conditions for Your present and future Contributions submitted to GitLab B.V. +Except for the license granted herein to GitLab B.V. and recipients of software +distributed by GitLab B.V., You reserve all right, title, and interest in and to +Your Contributions. All Contributions are subject to the following DCO + License +terms. -## Contributing +[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md) -Thank you for your interest in contributing to this GitLab project! We welcome -all contributions. By participating in this project, you agree to abide by the -[code of conduct](#code-of-conduct). - -## Contributor license agreement - -By submitting code as an individual you agree to the [individual contributor -license agreement][individual-agreement]. - -By submitting code as an entity you agree to the [corporate contributor license -agreement][corporate-agreement]. +_This notice should stay as the first item in the CONTRIBUTING.md file._ ## Code of conduct diff --git a/vendor/Dockerfile/LICENSE b/vendor/Dockerfile/LICENSE index d6c93c6fcf7..27a215686e7 100644 --- a/vendor/Dockerfile/LICENSE +++ b/vendor/Dockerfile/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +Copyright (c) 2011-2017 GitLab B.V. -Copyright (c) 2016-2017 GitLab.org +With regard to the GitLab Software: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,17 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +For all third party components incorporated into the GitLab Software, those +components are licensed under the original license provided by the owner of the +applicable component. diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore index cca150a88dd..7996ad5058e 100644 --- a/vendor/gitignore/Global/Matlab.gitignore +++ b/vendor/gitignore/Global/Matlab.gitignore @@ -1,5 +1,5 @@ ##--------------------------------------------------- -## Remove autosaves generated by the Matlab editor +## Remove autosaves generated by the MATLAB editor ## We have git for backups! ##--------------------------------------------------- @@ -14,6 +14,7 @@ # Simulink Code Generation slprj/ +sccprj/ # Session info octave-workspace diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore index a1338d68517..ea58090bd21 100644 --- a/vendor/gitignore/Go.gitignore +++ b/vendor/gitignore/Go.gitignore @@ -9,6 +9,3 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out - -# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 -.glide/ diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore index eee88b2f0f7..82f3a88e17b 100644 --- a/vendor/gitignore/Haskell.gitignore +++ b/vendor/gitignore/Haskell.gitignore @@ -17,5 +17,6 @@ cabal.sandbox.config *.eventlog .stack-work/ cabal.project.local +cabal.project.local~ .HTF/ .ghc.environment.* diff --git a/vendor/gitignore/Jekyll.gitignore b/vendor/gitignore/Jekyll.gitignore index 5c91b60c063..2ca868298ce 100644 --- a/vendor/gitignore/Jekyll.gitignore +++ b/vendor/gitignore/Jekyll.gitignore @@ -1,3 +1,4 @@ _site/ .sass-cache/ +.jekyll-cache/ .jekyll-metadata diff --git a/vendor/gitignore/ROS.gitignore b/vendor/gitignore/ROS.gitignore index f8bcd117371..425641f2c3a 100644 --- a/vendor/gitignore/ROS.gitignore +++ b/vendor/gitignore/ROS.gitignore @@ -1,3 +1,5 @@ +devel/ +logs/ build/ bin/ lib/ diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore index 85fd714a965..d098259ffb0 100644 --- a/vendor/gitignore/Symfony.gitignore +++ b/vendor/gitignore/Symfony.gitignore @@ -25,6 +25,7 @@ /bin/* !bin/console !bin/symfony_requirements +/vendor/ # Assets and user uploads /web/bundles/ @@ -37,6 +38,9 @@ # Build data /build/ +# Composer PHAR +/composer.phar + # Backup entities generated with doctrine:generate:entities command **/Entity/*~ diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore index b6418e51766..9bb63365618 100644 --- a/vendor/gitignore/TeX.gitignore +++ b/vendor/gitignore/TeX.gitignore @@ -215,7 +215,11 @@ TSWLatexianTemp* *~[0-9]* # auto folder when using emacs and auctex -/auto/* +./auto/* +*.el # expex forward references with \gathertags *-tags.tex + +# standalone packages +*.sta diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore index eb83a8f122d..75e5b1405da 100644 --- a/vendor/gitignore/Unity.gitignore +++ b/vendor/gitignore/Unity.gitignore @@ -1,9 +1,9 @@ -/[Ll]ibrary/ -/[Tt]emp/ -/[Oo]bj/ -/[Bb]uild/ -/[Bb]uilds/ -/Assets/AssetStoreTools* +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Bb]uild/ +[Bb]uilds/ +Assets/AssetStoreTools* # Visual Studio 2015 cache directory /.vs/ @@ -25,6 +25,7 @@ ExportedObj/ # Unity3D generated meta files *.pidb.meta +*.pdb.meta # Unity3D Generated File On Crash Reports sysinfo.txt diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore index 6c6e1c327fd..1daca8b50d9 100644 --- a/vendor/gitignore/UnrealEngine.gitignore +++ b/vendor/gitignore/UnrealEngine.gitignore @@ -50,6 +50,7 @@ SourceArt/**/*.tga # Binary Files Binaries/* +Plugins/*/Binaries/* # Builds Build/* @@ -70,6 +71,7 @@ Saved/* # Compiled source files for the engine to use Intermediate/* +Plugins/*/Intermediate/* # Cache files for the editor to use DerivedDataCache/* diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index 509668db67a..6217e6c48e9 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -24,11 +24,14 @@ bld/ [Oo]bj/ [Ll]og/ -# Visual Studio 2015 cache/options directory +# Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ +# Visual Studio 2017 auto generated files +Generated\ Files/ + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* @@ -51,6 +54,10 @@ project.fragment.lock.json artifacts/ **/Properties/launchSettings.json +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio *_i.c *_p.c *_i.h @@ -247,7 +254,7 @@ FakesAssemblies/ .ntvs_analysis.dat node_modules/ -# Typescript v1 declaration files +# TypeScript v1 declaration files typings/ # Visual Studio 6 build log @@ -303,3 +310,6 @@ __pycache__/ # OpenCover UI analysis results OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index 88261502d7f..da4d86b9a04 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -83,6 +83,16 @@ codequality: artifacts: paths: [codeclimate.json] +sast: + image: registry.gitlab.com/gitlab-org/gl-sast:latest + variables: + POSTGRES_DB: "false" + allow_failure: true + script: + - /app/bin/run . + artifacts: + paths: [gl-sast-report.json] + review: stage: review script: @@ -218,8 +228,8 @@ production: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume /tmp/cc:/tmp/cc" - docker run ${cc_opts} codeclimate/codeclimate init - docker run ${cc_opts} codeclimate/codeclimate analyze -f json > codeclimate.json + docker run ${cc_opts} codeclimate/codeclimate:0.69.0 init + docker run ${cc_opts} codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json } function deploy() { @@ -345,6 +355,13 @@ production: } function build() { + + if [[ -n "$CI_REGISTRY_USER" ]]; then + echo "Logging to GitLab Container Registry with CI credentials..." + docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" + echo "" + fi + if [[ -f Dockerfile ]]; then echo "Building Dockerfile-based application..." docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" . @@ -362,12 +379,6 @@ production: echo "" fi - if [[ -n "$CI_REGISTRY_USER" ]]; then - echo "Logging to GitLab Container Registry with CI credentials..." - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" - echo "" - fi - echo "Pushing to GitLab Container Registry..." docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" echo "" @@ -402,7 +413,9 @@ production: name="$name-$track" fi - helm delete "$name" || true + if [[ -n "$(helm ls -q "^$name$")" ]]; then + helm delete "$name" + fi } before_script: diff --git a/vendor/gitlab-ci-yml/CONTRIBUTING.md b/vendor/gitlab-ci-yml/CONTRIBUTING.md index d4c057bf9dc..d33a1f06f26 100644 --- a/vendor/gitlab-ci-yml/CONTRIBUTING.md +++ b/vendor/gitlab-ci-yml/CONTRIBUTING.md @@ -1,16 +1,15 @@ -## Contributing +## Developer Certificate of Origin + License -Thank you for your interest in contributing to this GitLab project! We welcome -all contributions. By participating in this project, you agree to abide by the -[code of conduct](#code-of-conduct). +By contributing to GitLab B.V., You accept and agree to the following terms and +conditions for Your present and future Contributions submitted to GitLab B.V. +Except for the license granted herein to GitLab B.V. and recipients of software +distributed by GitLab B.V., You reserve all right, title, and interest in and to +Your Contributions. All Contributions are subject to the following DCO + License +terms. -## Contributor license agreement +[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md) -By submitting code as an individual you agree to the [individual contributor -license agreement][individual-agreement]. - -By submitting code as an entity you agree to the [corporate contributor license -agreement][corporate-agreement]. +_This notice should stay as the first item in the CONTRIBUTING.md file._ ## Code of conduct diff --git a/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml new file mode 100644 index 00000000000..4d5b6484d6e --- /dev/null +++ b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml @@ -0,0 +1,51 @@ +# This file uses Test Kitchen with the kitchen-dokken driver to +# perform functional testing. Doing so requires that your runner be a +# Docker runner configured for privileged mode. Please see +# https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode +# for help configuring your runner properly, or, if you want to switch +# to a different driver, see http://kitchen.ci/docs/drivers + +image: "chef/chefdk" +services: + - docker:dind + +variables: + DOCKER_HOST: "tcp://docker:2375" + KITCHEN_LOCAL_YAML: ".kitchen.dokken.yml" + +stages: + - lint + - unit + - functional + +foodcritic: + stage: lint + script: + - chef exec foodcritic . + +cookstyle: + stage: lint + script: + - chef exec cookstyle . + +chefspec: + stage: unit + script: + - chef exec rspec spec + +# Set up your test matrix here. Example: +#verify-centos-6: +# stage: functional +# before_script: +# - apt-get update +# - apt-get -y install rsync +# script: +# - kitchen verify default-centos-6 --destroy=always +# +#verify-centos-7: +# stage: functional +# before_script: +# - apt-get update +# - apt-get -y install rsync +# script: +# - kitchen verify default-centos-7 --destroy=always diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml index 86e4985d8d2..d572d7a1edc 100644 --- a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml @@ -11,8 +11,8 @@ variables: # repository in /go/src/gitlab.com/namespace/project # Thus, making a symbolic link corrects this. before_script: - - mkdir -p $GOPATH/src/$REPO_NAME - - ln -svf $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME + - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) + - ln -svf $CI_PROJECT_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME stages: diff --git a/vendor/gitlab-ci-yml/LICENSE b/vendor/gitlab-ci-yml/LICENSE index d6c93c6fcf7..27a215686e7 100644 --- a/vendor/gitlab-ci-yml/LICENSE +++ b/vendor/gitlab-ci-yml/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +Copyright (c) 2011-2017 GitLab B.V. -Copyright (c) 2016-2017 GitLab.org +With regard to the GitLab Software: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,17 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +For all third party components incorporated into the GitLab Software, those +components are licensed under the original license provided by the owner of the +applicable component. diff --git a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml index 6573eceaa59..1463161a04b 100644 --- a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml @@ -20,4 +20,4 @@ image: "rust:latest" test:cargo: script: - rustc --version && cargo --version # Print version info for debugging - - cargo test --verbose --jobs 1 --release # Don't paralize to make errors more readable + - cargo test --verbose --jobs 1 --release # Don't parallelise to make errors more readable diff --git a/vendor/licenses.csv b/vendor/licenses.csv index 6f6ca5f8b32..b6a5c2f81a0 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -1,460 +1,78 @@ -"","","MIT,ISC,Apache 2.0,New BSD,Simplified BSD" RedCloth,4.3.2,MIT -abbrev,1.0.9,ISC -abbrev,1.1.0,ISC -accepts,1.3.3,MIT ace-rails-ap,4.1.2,MIT -acorn,3.3.0,MIT -acorn,4.0.13,MIT -acorn,5.1.1,MIT -acorn-dynamic-import,2.0.2,MIT -acorn-jsx,3.0.1,MIT -actionmailer,4.2.8,MIT -actionpack,4.2.8,MIT -actionview,4.2.8,MIT -activejob,4.2.8,MIT -activemodel,4.2.8,MIT -activerecord,4.2.8,MIT -activesupport,4.2.8,MIT +actionmailer,4.2.10,MIT +actionpack,4.2.10,MIT +actionview,4.2.10,MIT +activejob,4.2.10,MIT +activemodel,4.2.10,MIT +activerecord,4.2.10,MIT +activesupport,4.2.10,MIT acts-as-taggable-on,4.0.0,MIT addressable,2.5.2,Apache 2.0 -after,0.8.2,MIT -ajv,4.11.8,MIT -ajv,5.2.2,MIT -ajv-keywords,1.5.1,MIT -ajv-keywords,2.1.0,MIT akismet,2.0.0,MIT -align-text,0.1.4,MIT allocations,1.0.5,MIT -alphanum-sort,1.0.2,MIT -amdefine,1.0.1,BSD-3-Clause OR MIT -ansi-escapes,1.4.0,MIT -ansi-html,0.0.5,"Apache, Version 2.0" -ansi-html,0.0.7,Apache 2.0 -ansi-regex,2.1.1,MIT -ansi-styles,2.2.1,MIT -ansi-styles,3.2.0,MIT -anymatch,1.3.2,ISC -append-transform,0.4.0,MIT -aproba,1.1.1,ISC -are-we-there-yet,1.1.4,ISC arel,6.0.4,MIT -argparse,1.0.9,MIT -arr-diff,2.0.0,MIT -arr-flatten,1.0.1,MIT -array-find,1.0.0,MIT -array-find-index,1.0.2,MIT -array-flatten,1.1.1,MIT -array-flatten,2.1.1,MIT -array-slice,0.2.3,MIT -array-union,1.0.2,MIT -array-uniq,1.0.3,MIT -array-unique,0.2.1,MIT -arraybuffer.slice,0.0.6,MIT -arrify,1.0.1,MIT asana,0.6.0,MIT asciidoctor,1.5.3,MIT asciidoctor-plantuml,0.0.7,MIT -asn1,0.2.3,MIT -asn1.js,4.9.1,MIT -assert,1.4.1,MIT -assert-plus,0.2.0,MIT -assert-plus,1.0.0,MIT -async,0.9.2,MIT -async,1.5.2,MIT -async,2.4.1,MIT -async-each,1.0.1,MIT -asynckit,0.4.0,MIT +asset_sync,2.2.0,MIT atomic,1.1.99,Apache 2.0 attr_encrypted,3.0.3,MIT attr_required,1.0.0,MIT -autoprefixer,6.7.7,MIT autoprefixer-rails,6.2.3,MIT -autosize,4.0.0,MIT -aws-sign2,0.6.0,Apache 2.0 -aws4,1.6.0,MIT axiom-types,0.1.1,MIT -axios,0.16.2,MIT -babel-code-frame,6.22.0,MIT -babel-core,6.23.1,MIT -babel-eslint,7.2.1,MIT -babel-generator,6.23.0,MIT -babel-helper-bindify-decorators,6.22.0,MIT -babel-helper-builder-binary-assignment-operator-visitor,6.22.0,MIT -babel-helper-call-delegate,6.22.0,MIT -babel-helper-define-map,6.23.0,MIT -babel-helper-explode-assignable-expression,6.22.0,MIT -babel-helper-explode-class,6.22.0,MIT -babel-helper-function-name,6.23.0,MIT -babel-helper-get-function-arity,6.22.0,MIT -babel-helper-hoist-variables,6.22.0,MIT -babel-helper-optimise-call-expression,6.23.0,MIT -babel-helper-regex,6.22.0,MIT -babel-helper-remap-async-to-generator,6.22.0,MIT -babel-helper-replace-supers,6.23.0,MIT -babel-helpers,6.23.0,MIT -babel-loader,7.1.1,MIT -babel-messages,6.23.0,MIT -babel-plugin-check-es2015-constants,6.22.0,MIT -babel-plugin-istanbul,4.0.0,New BSD -babel-plugin-syntax-async-functions,6.13.0,MIT -babel-plugin-syntax-async-generators,6.13.0,MIT -babel-plugin-syntax-class-properties,6.13.0,MIT -babel-plugin-syntax-decorators,6.13.0,MIT -babel-plugin-syntax-dynamic-import,6.18.0,MIT -babel-plugin-syntax-exponentiation-operator,6.13.0,MIT -babel-plugin-syntax-object-rest-spread,6.13.0,MIT -babel-plugin-syntax-trailing-function-commas,6.22.0,MIT -babel-plugin-transform-async-generator-functions,6.22.0,MIT -babel-plugin-transform-async-to-generator,6.22.0,MIT -babel-plugin-transform-class-properties,6.23.0,MIT -babel-plugin-transform-decorators,6.22.0,MIT -babel-plugin-transform-define,1.2.0,MIT -babel-plugin-transform-es2015-arrow-functions,6.22.0,MIT -babel-plugin-transform-es2015-block-scoped-functions,6.22.0,MIT -babel-plugin-transform-es2015-block-scoping,6.23.0,MIT -babel-plugin-transform-es2015-classes,6.23.0,MIT -babel-plugin-transform-es2015-computed-properties,6.22.0,MIT -babel-plugin-transform-es2015-destructuring,6.23.0,MIT -babel-plugin-transform-es2015-duplicate-keys,6.22.0,MIT -babel-plugin-transform-es2015-for-of,6.23.0,MIT -babel-plugin-transform-es2015-function-name,6.22.0,MIT -babel-plugin-transform-es2015-literals,6.22.0,MIT -babel-plugin-transform-es2015-modules-amd,6.24.0,MIT -babel-plugin-transform-es2015-modules-commonjs,6.24.0,MIT -babel-plugin-transform-es2015-modules-systemjs,6.23.0,MIT -babel-plugin-transform-es2015-modules-umd,6.24.0,MIT -babel-plugin-transform-es2015-object-super,6.22.0,MIT -babel-plugin-transform-es2015-parameters,6.23.0,MIT -babel-plugin-transform-es2015-shorthand-properties,6.22.0,MIT -babel-plugin-transform-es2015-spread,6.22.0,MIT -babel-plugin-transform-es2015-sticky-regex,6.22.0,MIT -babel-plugin-transform-es2015-template-literals,6.22.0,MIT -babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT -babel-plugin-transform-es2015-unicode-regex,6.22.0,MIT -babel-plugin-transform-exponentiation-operator,6.22.0,MIT -babel-plugin-transform-object-rest-spread,6.23.0,MIT -babel-plugin-transform-regenerator,6.22.0,MIT -babel-plugin-transform-strict-mode,6.22.0,MIT -babel-preset-es2015,6.24.0,MIT -babel-preset-es2016,6.22.0,MIT -babel-preset-es2017,6.22.0,MIT -babel-preset-latest,6.24.0,MIT -babel-preset-stage-2,6.22.0,MIT -babel-preset-stage-3,6.22.0,MIT -babel-register,6.23.0,MIT -babel-runtime,6.22.0,MIT -babel-template,6.23.0,MIT -babel-traverse,6.23.1,MIT -babel-types,6.23.0,MIT babosa,1.0.2,MIT -babylon,6.16.1,MIT -backo2,1.0.2,MIT -balanced-match,0.4.2,MIT -balanced-match,1.0.0,MIT base32,0.3.2,MIT -base64-arraybuffer,0.1.5,MIT -base64-js,1.2.0,MIT -base64id,1.0.0,MIT -batch,0.6.1,MIT +batch-loader,1.1.1,MIT bcrypt,3.1.11,MIT -bcrypt-pbkdf,1.0.1,New BSD bcrypt_pbkdf,1.0.0,MIT -better-assert,1.0.2,MIT -big.js,3.1.3,MIT -binary-extensions,1.10.0,MIT bindata,2.4.1,ruby -blob,0.0.4,unknown -block-stream,0.0.9,ISC -bluebird,2.11.0,MIT -bluebird,3.5.0,MIT -bn.js,4.11.6,MIT -body-parser,1.17.2,MIT -bonjour,3.5.0,MIT -boom,2.10.1,New BSD bootstrap-sass,3.3.6,MIT bootstrap_form,2.7.0,MIT -brace-expansion,1.1.7,MIT -brace-expansion,1.1.8,MIT -braces,0.1.5,MIT -braces,1.8.5,MIT -brorand,1.0.7,MIT browser,2.2.0,MIT -browserify-aes,1.0.6,MIT -browserify-cipher,1.0.0,MIT -browserify-des,1.0.0,MIT -browserify-rsa,4.0.1,MIT -browserify-sign,4.0.0,ISC -browserify-zlib,0.1.4,MIT -browserslist,1.7.7,MIT -buffer,4.9.1,MIT -buffer-indexof,1.1.0,MIT -buffer-shims,1.0.0,MIT -buffer-xor,1.0.3,MIT builder,3.2.3,MIT -builtin-modules,1.1.1,MIT -builtin-status-codes,3.0.0,MIT -bytes,2.4.0,MIT -bytes,2.5.0,MIT -caller-path,0.1.0,MIT -callsite,1.0.0,unknown -callsites,0.2.0,MIT -camelcase,1.2.1,MIT -camelcase,2.1.1,MIT -camelcase,3.0.0,MIT -camelcase,4.1.0,MIT -camelcase-keys,2.1.0,MIT -caniuse-api,1.6.1,MIT -caniuse-db,1.0.30000649,CC-BY-4.0 carrierwave,1.2.1,MIT -caseless,0.12.0,Apache 2.0 cause,0.1,MIT -center-align,0.1.3,MIT -chalk,1.1.3,MIT -chalk,2.3.0,MIT charlock_holmes,0.7.5,MIT -chokidar,1.7.0,MIT chronic,0.10.2,MIT chronic_duration,0.10.6,MIT chunky_png,1.3.5,MIT -cipher-base,1.0.3,MIT -circular-json,0.3.3,MIT citrus,3.0.2,MIT -clap,1.1.3,MIT -cli-cursor,1.0.2,MIT -cli-width,2.1.0,ISC -clipboard,1.6.1,MIT -cliui,2.1.0,ISC -cliui,3.2.0,ISC -clone,1.0.2,MIT -co,4.6.0,MIT -coa,1.0.1,MIT -code-point-at,1.1.0,MIT coercible,1.0.0,MIT -color,0.11.4,MIT -color-convert,1.9.0,MIT -color-name,1.1.2,MIT -color-string,0.3.0,MIT -colormin,1.1.2,MIT -colors,1.1.2,MIT -combine-lists,1.0.1,MIT -combined-stream,1.0.5,MIT -commander,2.9.0,MIT -commondir,1.0.1,MIT -component-bind,1.0.0,unknown -component-emitter,1.1.2,unknown -component-emitter,1.2.1,MIT -component-inherit,0.0.3,unknown -compressible,2.0.11,MIT -compression,1.7.0,MIT -compression-webpack-plugin,1.0.0,MIT -concat-map,0.0.1,MIT -concat-stream,1.6.0,MIT concurrent-ruby-ext,1.0.5,MIT -config-chain,1.1.11,MIT -configstore,1.4.0,Simplified BSD -connect,3.6.3,MIT -connect-history-api-fallback,1.3.0,MIT connection_pool,2.2.1,MIT -console-browserify,1.1.0,MIT -console-control-strings,1.1.0,ISC -consolidate,0.14.5,MIT -constants-browserify,1.0.0,MIT -contains-path,0.1.0,MIT -content-disposition,0.5.2,MIT -content-type,1.0.2,MIT -convert-source-map,1.3.0,MIT -cookie,0.3.1,MIT -cookie-signature,1.0.6,MIT -copy-webpack-plugin,4.0.1,MIT -core-js,2.3.0,MIT -core-js,2.4.1,MIT -core-util-is,1.0.2,MIT -cosmiconfig,2.1.1,MIT crack,0.4.3,MIT -create-ecdh,4.0.0,MIT -create-hash,1.1.2,MIT -create-hmac,1.1.4,MIT creole,0.5.0,ruby -cropper,2.3.0,MIT -cross-spawn,5.1.0,MIT -cryptiles,2.0.5,New BSD -crypto-browserify,3.11.0,MIT -css-color-names,0.0.4,MIT -css-loader,0.28.0,MIT -css-selector-tokenizer,0.6.0,MIT -css-selector-tokenizer,0.7.0,MIT css_parser,1.5.0,MIT -cssesc,0.1.0,MIT -cssnano,3.10.0,MIT -csso,2.3.2,MIT -currently-unhandled,0.4.1,MIT -custom-event,1.0.1,MIT -d,0.1.1,MIT -d,1.0.0,MIT -d3,3.5.11,New BSD d3_rails,3.5.11,MIT -dashdash,1.14.1,MIT -date-now,0.1.4,MIT -de-indent,1.0.2,MIT -debug,2.2.0,MIT -debug,2.3.3,MIT -debug,2.6.7,MIT -debug,2.6.8,MIT debugger-ruby_core_source,1.3.8,MIT -decamelize,1.2.0,MIT deckar01-task_list,2.0.0,MIT declarative,0.0.10,MIT declarative-option,0.1.0,MIT -decompress-response,3.3.0,MIT -deep-equal,1.0.1,MIT -deep-extend,0.4.2,MIT -deep-is,0.1.3,MIT -default-require-extensions,1.0.0,MIT default_value_for,3.0.2,MIT -defined,1.0.0,MIT -del,2.2.2,MIT -del,3.0.0,MIT -delayed-stream,1.0.0,MIT -delegate,3.1.2,MIT -delegates,1.0.0,MIT -depd,1.1.0,MIT -depd,1.1.1,MIT -des.js,1.0.0,MIT descendants_tracker,0.0.4,MIT -destroy,1.0.4,MIT -detect-indent,4.0.0,MIT -detect-node,2.0.3,ISC devise,4.2.0,MIT devise-two-factor,3.0.0,MIT -di,0.0.1,MIT diff-lcs,1.3,"MIT,Artistic-2.0,GPL-2.0+" -diffie-hellman,5.0.2,MIT diffy,3.1.0,MIT -dns-equal,1.0.0,MIT -dns-packet,1.2.2,MIT -dns-txt,2.0.2,MIT -doctrine,1.5.0,BSD -doctrine,2.0.0,Apache 2.0 -document-register-element,1.3.0,MIT -dom-serialize,2.2.1,MIT -dom-serializer,0.1.0,MIT -domain-browser,1.1.7,MIT domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0" -domelementtype,1.1.3,unknown -domelementtype,1.3.0,unknown -domhandler,2.3.0,unknown -domutils,1.5.1,unknown doorkeeper,4.2.6,MIT doorkeeper-openid_connect,1.2.0,MIT -dropzone,4.2.0,MIT dropzonejs-rails,0.7.2,MIT -duplexer,0.1.1,MIT -duplexer3,0.1.4,New BSD -duplexify,3.5.1,MIT -ecc-jsbn,0.1.1,MIT -editorconfig,0.13.2,MIT -ee-first,1.1.1,MIT -ejs,2.5.6,Apache 2.0 -electron-to-chromium,1.3.3,ISC -elliptic,6.3.3,MIT email_reply_trimmer,0.1.6,MIT -emoji-unicode-version,0.2.1,MIT -emojis-list,2.1.0,MIT -encodeurl,1.0.1,MIT encryptor,3.0.0,MIT -end-of-stream,1.4.0,MIT -engine.io,1.8.3,MIT -engine.io-client,1.8.3,MIT -engine.io-parser,1.3.2,MIT -enhanced-resolve,0.9.1,MIT -enhanced-resolve,3.4.1,MIT -ent,2.2.0,MIT -entities,1.1.1,BSD-like equalizer,0.0.11,MIT -errno,0.1.4,MIT -error-ex,1.3.0,MIT erubis,2.7.0,MIT -es5-ext,0.10.24,MIT -es6-iterator,2.0.1,MIT -es6-map,0.1.5,MIT -es6-promise,3.0.2,MIT -es6-set,0.1.5,MIT -es6-symbol,3.1.1,MIT -es6-weak-map,2.0.1,MIT -escape-html,1.0.3,MIT -escape-string-regexp,1.0.5,MIT escape_utils,1.1.1,MIT -escodegen,1.8.1,Simplified BSD -escope,3.6.0,Simplified BSD -eslint,3.19.0,MIT -eslint-config-airbnb-base,10.0.1,MIT -eslint-import-resolver-node,0.2.3,MIT -eslint-import-resolver-webpack,0.8.3,MIT -eslint-module-utils,2.0.0,MIT -eslint-plugin-filenames,1.1.0,MIT -eslint-plugin-html,2.0.1,ISC -eslint-plugin-import,2.2.0,MIT -eslint-plugin-jasmine,2.2.0,MIT -eslint-plugin-promise,3.5.0,ISC -espree,3.5.0,Simplified BSD -esprima,2.7.3,Simplified BSD -esprima,4.0.0,Simplified BSD -esquery,1.0.0,BSD -esrecurse,4.1.0,Simplified BSD -estraverse,1.9.3,BSD -estraverse,4.1.1,Simplified BSD -estraverse,4.2.0,Simplified BSD -esutils,2.0.2,BSD et-orbi,1.0.3,MIT -etag,1.8.0,MIT -eve-raphael,0.5.0,Apache 2.0 -event-emitter,0.3.5,MIT -event-stream,3.3.4,MIT -eventemitter3,1.2.0,MIT -events,1.1.1,MIT -eventsource,0.1.6,MIT -evp_bytestokey,1.0.0,MIT excon,0.57.1,MIT -execa,0.7.0,MIT execjs,2.6.0,MIT -exit-hook,1.1.1,MIT -expand-braces,0.1.2,MIT -expand-brackets,0.1.5,MIT -expand-range,0.1.1,MIT -expand-range,1.8.2,MIT -exports-loader,0.6.4,MIT -express,4.15.4,MIT expression_parser,0.9.0,MIT -extend,3.0.1,MIT -extglob,0.3.2,MIT -extsprintf,1.0.2,MIT faraday,0.12.2,MIT faraday_middleware,0.11.0.1,MIT faraday_middleware-multi_json,0.0.6,MIT -fast-deep-equal,1.0.0,MIT -fast-levenshtein,2.0.6,MIT fast_gettext,1.4.0,"MIT,ruby" -fastparse,1.1.1,MIT -faye-websocket,0.10.0,MIT -faye-websocket,0.11.1,MIT -faye-websocket,0.7.3,MIT ffi,1.9.18,New BSD -figures,1.7.0,MIT -file-entry-cache,2.0.0,MIT -file-loader,0.11.1,MIT -filename-regex,2.0.0,MIT -fileset,2.0.3,MIT -filesize,3.3.0,New BSD -filesize,3.5.10,New BSD -fill-range,2.2.3,MIT -finalhandler,1.0.4,MIT -find-cache-dir,1.0.0,MIT -find-root,0.1.2,MIT -find-up,1.1.2,MIT -find-up,2.1.0,MIT -flat-cache,1.2.2,MIT -flatten,1.0.2,MIT flipper,0.10.2,MIT flipper-active_record,0.10.2,MIT flowdock,0.7.1,MIT @@ -467,392 +85,93 @@ fog-local,0.3.1,MIT fog-openstack,0.1.21,MIT fog-rackspace,0.1.1,MIT fog-xml,0.1.3,MIT -follow-redirects,1.2.3,MIT font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License" -for-in,0.1.6,MIT -for-own,0.1.4,MIT -forever-agent,0.6.1,Apache 2.0 -form-data,2.1.4,MIT formatador,0.2.5,MIT -forwarded,0.1.0,MIT -fresh,0.5.0,MIT -from,0.1.7,MIT -fs-access,1.0.1,MIT -fs-extra,0.26.7,MIT -fs.realpath,1.0.0,ISC -fsevents,1.1.2,MIT -fstream,1.0.11,ISC -fstream-ignore,1.0.5,ISC -function-bind,1.1.0,MIT -fuzzaldrin-plus,0.5.0,MIT -gauge,2.7.4,ISC gemnasium-gitlab-service,0.2.6,MIT gemojione,3.3.0,MIT -generate-function,2.0.0,MIT -generate-object-property,1.2.0,MIT -get-caller-file,1.0.2,ISC -get-stdin,4.0.1,MIT -get-stream,3.0.0,MIT get_process_mem,0.2.0,MIT -getpass,0.1.7,MIT gettext_i18n_rails,1.8.0,MIT gettext_i18n_rails_js,1.2.0,MIT -gitaly-proto,0.51.0,MIT +gitaly-proto,0.59.0,MIT github-linguist,4.7.6,MIT github-markup,1.6.1,MIT gitlab-flowdock-git-hook,1.0.1,MIT gitlab-grit,2.8.2,MIT gitlab-markup,1.6.3,MIT -gitlab-svgs,1.0.4,unknown gitlab_omniauth-ldap,2.0.4,MIT -glob,5.0.15,ISC -glob,6.0.4,ISC -glob,7.1.1,ISC -glob,7.1.2,ISC -glob-base,0.3.0,MIT -glob-parent,2.0.0,ISC -globalid,0.3.7,MIT -globals,9.18.0,MIT -globby,5.0.0,MIT -globby,6.1.0,MIT +globalid,0.4.1,MIT gollum-grit_adapter,1.0.1,MIT gollum-lib,4.2.7,MIT gollum-rugged_adapter,0.4.4,MIT gon,6.1.0,MIT -good-listener,1.2.2,MIT google-api-client,0.13.6,Apache 2.0 google-protobuf,3.4.1.1,New BSD -googleapis-common-protos-types,1.0.0,Apache 2.0 googleauth,0.5.3,Apache 2.0 -got,3.3.1,MIT -got,7.1.0,MIT gpgme,2.0.13,LGPL-2.1+ -graceful-fs,4.1.11,ISC -graceful-readlink,1.0.1,MIT grape,1.0.0,MIT grape-entity,0.6.0,MIT grape-route-helpers,2.1.0,MIT grape_logging,1.7.0,MIT -grpc,1.6.6,Apache 2.0 -gzip-size,3.0.0,MIT +grpc,1.4.5,New BSD hamlit,2.6.1,MIT -handle-thing,1.2.5,MIT -handlebars,4.0.6,MIT -har-schema,1.0.5,ISC -har-validator,4.2.1,ISC -has,1.0.1,MIT -has-ansi,2.0.0,MIT -has-binary,0.1.7,MIT -has-cors,1.1.0,MIT -has-flag,1.0.0,MIT -has-flag,2.0.0,MIT -has-symbol-support-x,1.3.0,MIT -has-to-string-tag-x,1.3.0,MIT -has-unicode,2.0.1,ISC -hash-sum,1.0.2,MIT -hash.js,1.0.3,MIT hashie,3.5.6,MIT hashie-forbidden_attributes,0.1.1,MIT -hawk,3.1.3,New BSD -he,1.1.1,MIT health_check,2.6.0,MIT hipchat,1.5.2,MIT -hoek,2.16.3,New BSD -home-or-tmp,2.0.0,MIT -hosted-git-info,2.2.0,ISC -hpack.js,2.1.6,MIT -html-comment-regex,1.1.1,MIT -html-entities,1.2.0,MIT html-pipeline,1.11.0,MIT html2text,0.2.0,MIT htmlentities,4.3.4,MIT -htmlparser2,3.9.2,MIT http,0.9.8,MIT http-cookie,1.0.3,MIT -http-deceiver,1.2.7,MIT -http-errors,1.6.1,MIT -http-errors,1.6.2,MIT http-form_data,1.0.1,MIT -http-proxy,1.16.2,MIT -http-proxy-middleware,0.17.4,MIT -http-signature,1.1.1,MIT http_parser.rb,0.6.0,MIT httparty,0.13.7,MIT httpclient,2.8.2,ruby -https-browserify,0.0.1,MIT -i18n,0.8.6,MIT +i18n,0.9.1,MIT ice_nine,0.11.2,MIT -iconv-lite,0.4.15,MIT -icss-replace-symbols,1.0.2,ISC -ieee754,1.1.8,New BSD -ignore,3.3.3,MIT -ignore-by-default,1.0.1,ISC -immediate,3.0.6,MIT -imports-loader,0.7.1,MIT -imurmurhash,0.1.4,MIT -indent-string,2.1.0,MIT -indexes-of,1.0.1,MIT -indexof,0.0.1,unknown -infinity-agent,2.0.3,MIT -inflight,1.0.6,ISC influxdb,0.2.3,MIT -inherits,2.0.1,ISC -inherits,2.0.3,ISC -ini,1.3.4,ISC -inquirer,0.12.0,MIT -internal-ip,1.2.0,MIT -interpret,1.0.1,MIT -invariant,2.2.2,New BSD -invert-kv,1.0.0,MIT -ip,1.1.5,MIT -ipaddr.js,1.4.0,MIT ipaddress,0.8.3,MIT -is-absolute,0.2.6,MIT -is-absolute-url,2.1.0,MIT -is-arrayish,0.2.1,MIT -is-binary-path,1.0.1,MIT -is-buffer,1.1.5,MIT -is-builtin-module,1.0.0,MIT -is-dotfile,1.0.2,MIT -is-equal-shallow,0.1.3,MIT -is-extendable,0.1.1,MIT -is-extglob,1.0.0,MIT -is-extglob,2.1.1,MIT -is-finite,1.0.2,MIT -is-fullwidth-code-point,1.0.0,MIT -is-fullwidth-code-point,2.0.0,MIT -is-glob,2.0.1,MIT -is-glob,3.1.0,MIT -is-my-json-valid,2.16.0,MIT -is-npm,1.0.0,MIT -is-number,0.1.1,MIT -is-number,2.1.0,MIT -is-object,1.0.1,MIT -is-path-cwd,1.0.0,MIT -is-path-in-cwd,1.0.0,MIT -is-path-inside,1.0.0,MIT -is-plain-obj,1.1.0,MIT -is-posix-bracket,0.1.1,MIT -is-primitive,2.0.0,MIT -is-property,1.0.2,MIT -is-redirect,1.0.0,MIT -is-relative,0.2.1,MIT -is-resolvable,1.0.0,MIT -is-retry-allowed,1.1.0,MIT -is-stream,1.1.0,MIT -is-svg,2.1.0,MIT -is-typedarray,1.0.0,MIT -is-unc-path,0.1.2,MIT -is-utf8,0.2.1,MIT -is-windows,0.2.0,MIT -isarray,0.0.1,MIT -isarray,1.0.0,MIT -isbinaryfile,3.0.2,MIT -isexe,1.1.2,ISC -isobject,2.1.0,MIT -isstream,0.1.2,MIT -istanbul,0.4.5,New BSD -istanbul-api,1.1.1,New BSD -istanbul-lib-coverage,1.0.1,New BSD -istanbul-lib-hook,1.0.0,New BSD -istanbul-lib-instrument,1.4.2,New BSD -istanbul-lib-report,1.0.0-alpha.3,New BSD -istanbul-lib-source-maps,1.1.0,New BSD -istanbul-reports,1.0.1,New BSD -isurl,1.0.0,MIT -jasmine-core,2.6.3,MIT -jasmine-jquery,2.1.1,MIT -jed,1.1.1,MIT jira-ruby,1.4.1,MIT -jodid25519,1.0.2,MIT -jquery,2.2.1,MIT jquery-atwho-rails,1.3.2,MIT -jquery-rails,4.1.1,MIT -jquery-ujs,1.2.1,MIT -js-base64,2.1.9,BSD -js-beautify,1.6.12,MIT -js-cookie,2.1.3,MIT -js-tokens,3.0.1,MIT -js-yaml,3.7.0,MIT -js-yaml,3.9.1,MIT -jsbn,0.1.1,MIT -jsesc,0.5.0,MIT -jsesc,1.3.0,MIT +jquery-rails,4.3.1,MIT json,1.8.6,ruby json-jwt,1.7.2,MIT -json-loader,0.5.7,MIT -json-schema,0.2.3,"AFLv2.1,BSD" -json-schema-traverse,0.3.1,MIT -json-stable-stringify,1.0.1,MIT -json-stringify-safe,5.0.1,ISC -json3,3.3.2,MIT -json5,0.5.1,MIT -jsonfile,2.4.0,MIT -jsonify,0.0.0,Public Domain -jsonpointer,4.0.1,MIT -jsprim,1.4.0,MIT -jszip,3.1.3,(MIT OR GPL-3.0) -jszip-utils,0.0.2,MIT or GPLv3 jwt,1.5.6,MIT kaminari,1.0.1,MIT kaminari-actionview,1.0.1,MIT kaminari-activerecord,1.0.1,MIT kaminari-core,1.0.1,MIT -karma,1.7.0,MIT -karma-chrome-launcher,2.1.1,MIT -karma-coverage-istanbul-reporter,0.2.0,MIT -karma-jasmine,1.1.0,MIT -karma-mocha-reporter,2.2.2,MIT -karma-sourcemap-loader,0.3.7,MIT -karma-webpack,2.0.4,MIT kgio,2.10.0,LGPL-2.1+ -kind-of,3.1.0,MIT -klaw,1.3.1,MIT kubeclient,2.2.0,MIT -latest-version,1.0.1,MIT -lazy-cache,1.0.4,MIT -lcid,1.0.0,MIT -levn,0.3.0,MIT licensee,8.7.0,MIT -lie,3.1.1,MIT little-plugger,1.1.4,MIT -load-json-file,1.1.0,MIT -load-json-file,2.0.0,MIT -loader-runner,2.3.0,MIT -loader-utils,0.2.16,MIT -loader-utils,1.1.0,MIT locale,2.1.2,"ruby,LGPLv3+" -locate-path,2.0.0,MIT -lodash,3.10.1,MIT -lodash,4.17.4,MIT -lodash._baseassign,3.2.0,MIT -lodash._basecopy,3.0.1,MIT -lodash._baseget,3.7.2,MIT -lodash._bindcallback,3.0.1,MIT -lodash._createassigner,3.1.1,MIT -lodash._getnative,3.9.1,MIT -lodash._isiterateecall,3.0.9,MIT -lodash._topath,3.8.1,MIT -lodash.assign,3.2.0,MIT -lodash.camelcase,4.1.1,MIT -lodash.camelcase,4.3.0,MIT -lodash.capitalize,4.2.1,MIT -lodash.cond,4.5.2,MIT -lodash.deburr,4.1.0,MIT -lodash.defaults,3.1.2,MIT -lodash.get,3.7.0,MIT -lodash.get,4.4.2,MIT -lodash.isarguments,3.1.0,MIT -lodash.isarray,3.0.4,MIT -lodash.kebabcase,4.0.1,MIT -lodash.keys,3.1.2,MIT -lodash.memoize,4.1.2,MIT -lodash.restparam,3.6.1,MIT -lodash.snakecase,4.0.1,MIT -lodash.uniq,4.5.0,MIT -lodash.words,4.2.0,MIT -log4js,0.6.38,Apache 2.0 logging,2.2.2,MIT -loglevel,1.4.1,MIT lograge,0.5.1,MIT -longest,1.0.1,MIT loofah,2.0.3,MIT -loose-envify,1.3.1,MIT -loud-rejection,1.6.0,MIT -lowercase-keys,1.0.0,MIT -lru-cache,2.2.4,MIT -lru-cache,3.2.0,ISC -lru-cache,4.0.2,ISC -macaddress,0.2.8,MIT -mail,2.6.6,MIT +mail,2.7.0,MIT mail_room,0.9.1,MIT -make-dir,1.0.0,MIT -map-obj,1.0.1,MIT -map-stream,0.1.0,unknown -marked,0.3.6,MIT -math-expression-evaluator,1.2.16,MIT -media-typer,0.3.0,MIT -mem,1.1.0,MIT memoist,0.16.0,MIT -memory-fs,0.2.0,MIT -memory-fs,0.4.1,MIT -meow,3.7.0,MIT -merge-descriptors,1.0.1,MIT method_source,0.8.2,MIT -methods,1.1.2,MIT -micromatch,2.3.11,MIT -miller-rabin,4.0.0,MIT -mime,1.3.4,MIT -mime-db,1.27.0,MIT -mime-db,1.29.0,MIT -mime-types,2.1.15,MIT mime-types,3.1,MIT mime-types-data,3.2016.0521,MIT mimemagic,0.3.0,MIT -mimic-fn,1.1.0,MIT -mimic-response,1.0.0,MIT +mini_mime,0.1.4,MIT mini_portile2,2.3.0,MIT -minimalistic-assert,1.0.0,ISC -minimatch,3.0.3,ISC -minimatch,3.0.4,ISC -minimist,0.0.8,MIT -minimist,1.2.0,MIT -mkdirp,0.5.1,MIT -mmap2,2.2.7,ruby -moment,2.17.1,MIT -monaco-editor,0.10.0,MIT -mousetrap,1.4.6,Apache 2.0 mousetrap-rails,1.4.6,"MIT,Apache" -ms,0.7.1,MIT -ms,0.7.2,MIT -ms,2.0.0,MIT multi_json,1.12.2,MIT multi_xml,0.6.0,MIT -multicast-dns,6.1.1,MIT -multicast-dns-service-types,1.1.0,MIT multipart-post,2.0.0,MIT mustermann,1.0.0,MIT mustermann-grape,1.0.0,MIT -mute-stream,0.0.5,ISC mysql2,0.4.5,MIT -name-all-modules-plugin,1.0.1,MIT -nan,2.6.2,MIT -natural-compare,1.4.0,MIT -negotiator,0.6.1,MIT -nested-error-stacks,1.0.2,MIT net-ldap,0.16.0,MIT net-ssh,4.1.0,MIT netrc,0.11.0,MIT -node-dir,0.1.17,MIT -node-forge,0.6.33,BSD -node-libs-browser,1.1.1,MIT -node-libs-browser,2.0.0,MIT -node-pre-gyp,0.6.36,New BSD -node-pre-gyp,0.6.37,New BSD -nodemon,1.11.0,MIT nokogiri,1.8.1,MIT -nopt,1.0.10,MIT -nopt,3.0.6,ISC -nopt,4.0.1,ISC -normalize-package-data,2.4.0,Simplified BSD -normalize-path,2.1.1,MIT -normalize-range,0.1.2,MIT -normalize-url,1.9.1,MIT -npm-run-path,2.0.2,MIT -npmlog,4.1.0,ISC -null-check,1.0.0,MIT -num2fraction,1.2.2,MIT -number-is-nan,1.0.1,MIT numerizer,0.1.1,MIT oauth,0.5.1,MIT -oauth-sign,0.8.2,Apache 2.0 oauth2,1.4.0,MIT -object-assign,3.0.0,MIT -object-assign,4.1.0,MIT -object-assign,4.1.1,MIT -object-component,0.0.3,unknown -object.omit,2.0.1,MIT -obuf,1.1.1,MIT octokit,4.6.2,MIT oj,2.17.5,MIT omniauth,1.4.2,MIT @@ -873,54 +192,10 @@ omniauth-saml,1.7.0,MIT omniauth-shibboleth,1.2.1,MIT omniauth-twitter,1.2.1,MIT omniauth_crowd,2.2.3,MIT -on-finished,2.3.0,MIT -on-headers,1.0.1,MIT -once,1.4.0,ISC -onetime,1.1.0,MIT -opener,1.4.3,(WTFPL OR MIT) -opn,4.0.2,MIT -optimist,0.6.1,MIT/X11 -optionator,0.8.2,MIT -options,0.0.6,MIT org-ruby,0.9.12,MIT -original,1.0.0,MIT orm_adapter,0.5.0,MIT os,0.9.6,MIT -os-browserify,0.2.1,MIT -os-homedir,1.0.2,MIT -os-locale,1.4.0,MIT -os-locale,2.1.0,MIT -os-tmpdir,1.0.2,MIT -osenv,0.1.4,ISC -p-cancelable,0.3.0,MIT -p-finally,1.0.0,MIT -p-limit,1.1.0,MIT -p-locate,2.0.0,MIT -p-map,1.1.1,MIT -p-timeout,1.2.0,MIT -package-json,1.2.0,MIT -pako,0.2.9,MIT -pako,1.0.5,(MIT AND Zlib) paranoia,2.3.1,MIT -parse-asn1,5.0.0,ISC -parse-glob,3.0.4,MIT -parse-json,2.2.0,MIT -parsejson,0.0.3,MIT -parseqs,0.0.5,MIT -parseuri,0.0.5,MIT -parseurl,1.3.1,MIT -path-browserify,0.0.0,MIT -path-exists,2.1.0,MIT -path-exists,3.0.0,MIT -path-is-absolute,1.0.1,MIT -path-is-inside,1.0.2,(WTFPL OR MIT) -path-key,2.0.1,MIT -path-parse,1.0.5,MIT -path-to-regexp,0.1.7,MIT -path-type,1.1.0,MIT -path-type,2.0.0,MIT -pause-stream,0.0.11,"MIT,Apache2" -pbkdf2,3.0.9,MIT peek,1.0.1,MIT peek-gc,0.0.2,MIT peek-host,1.0.0,MIT @@ -930,86 +205,14 @@ peek-pg,1.3.0,MIT peek-rblineprof,0.2.0,MIT peek-redis,1.2.0,MIT peek-sidekiq,1.0.3,MIT -performance-now,0.2.0,MIT pg,0.18.4,"BSD,ruby,GPL" -pify,2.3.0,MIT -pify,3.0.0,MIT -pikaday,1.6.1,MIT -pinkie,2.0.4,MIT -pinkie-promise,2.0.1,MIT -pkg-dir,1.0.0,MIT -pkg-dir,2.0.0,MIT -pkg-up,1.0.0,MIT -pluralize,1.2.1,MIT po_to_json,1.0.1,MIT -portfinder,1.0.13,MIT posix-spawn,0.3.13,MIT -postcss,5.2.16,MIT -postcss-calc,5.3.1,MIT -postcss-colormin,2.2.2,MIT -postcss-convert-values,2.6.1,MIT -postcss-discard-comments,2.0.4,MIT -postcss-discard-duplicates,2.1.0,MIT -postcss-discard-empty,2.1.0,MIT -postcss-discard-overridden,0.1.1,MIT -postcss-discard-unused,2.2.3,MIT -postcss-filter-plugins,2.0.2,MIT -postcss-load-config,1.2.0,MIT -postcss-load-options,1.2.0,MIT -postcss-load-plugins,2.3.0,MIT -postcss-merge-idents,2.1.7,MIT -postcss-merge-longhand,2.0.2,MIT -postcss-merge-rules,2.1.2,MIT -postcss-message-helpers,2.0.0,MIT -postcss-minify-font-values,1.0.5,MIT -postcss-minify-gradients,1.0.5,MIT -postcss-minify-params,1.2.2,MIT -postcss-minify-selectors,2.1.1,MIT -postcss-modules-extract-imports,1.0.1,ISC -postcss-modules-local-by-default,1.1.1,MIT -postcss-modules-scope,1.0.2,ISC -postcss-modules-values,1.2.2,ISC -postcss-normalize-charset,1.1.1,MIT -postcss-normalize-url,3.0.8,MIT -postcss-ordered-values,2.2.3,MIT -postcss-reduce-idents,2.4.0,MIT -postcss-reduce-initial,1.0.1,MIT -postcss-reduce-transforms,1.0.4,MIT -postcss-selector-parser,2.2.3,MIT -postcss-svgo,2.1.6,MIT -postcss-unique-selectors,2.0.2,MIT -postcss-value-parser,3.3.0,MIT -postcss-zindex,2.2.0,MIT -prelude-ls,1.1.2,MIT premailer,1.10.4,New BSD premailer-rails,1.9.7,MIT -prepend-http,1.0.4,MIT -preserve,0.2.0,MIT -prismjs,1.6.0,MIT -private,0.1.7,MIT -process,0.11.9,MIT -process-nextick-args,1.0.7,MIT -progress,1.1.8,MIT -prometheus-client-mmap,0.7.0.beta18,Apache 2.0 -proto-list,1.2.4,ISC -proxy-addr,1.1.5,MIT -prr,0.0.0,MIT -ps-tree,1.1.0,MIT -pseudomap,1.0.2,ISC -public-encrypt,4.0.0,MIT +prometheus-client-mmap,0.7.0.beta43,Apache 2.0 public_suffix,3.0.0,MIT -punycode,1.3.2,MIT -punycode,1.4.1,MIT pyu-ruby-sasl,0.0.3.3,MIT -q,1.5.0,MIT -qjobs,1.1.5,MIT -qs,6.4.0,New BSD -qs,6.5.0,New BSD -query-string,4.3.2,MIT -querystring,0.2.0,MIT -querystring-es3,0.2.1,MIT -querystringify,0.0.4,MIT -querystringify,1.0.0,MIT rack,1.6.8,MIT rack-accept,0.4.5,MIT rack-attack,4.4.1,MIT @@ -1018,88 +221,35 @@ rack-oauth2,1.2.3,MIT rack-protection,1.5.3,MIT rack-proxy,0.6.0,MIT rack-test,0.6.3,MIT -rails,4.2.8,MIT +rails,4.2.10,MIT rails-deprecated_sanitizer,1.0.3,MIT rails-dom-testing,1.0.8,MIT rails-html-sanitizer,1.0.3,MIT rails-i18n,4.0.9,MIT -railties,4.2.8,MIT +railties,4.2.10,MIT rainbow,2.2.2,MIT raindrops,0.18.0,LGPL-2.1+ -rake,12.1.0,MIT -randomatic,1.1.6,MIT -randombytes,2.0.3,MIT -range-parser,1.2.0,MIT -raphael,2.2.7,MIT -raven-js,3.14.0,Simplified BSD -raw-body,2.2.0,MIT -raw-loader,0.5.1,MIT +rake,12.3.0,MIT rbnacl,4.0.2,MIT rbnacl-libsodium,1.0.11,MIT -rc,1.2.1,(BSD-2-Clause OR MIT OR Apache-2.0) rdoc,4.2.2,ruby re2,1.1.1,New BSD -react-dev-utils,0.5.2,New BSD -read-all-stream,3.1.0,MIT -read-pkg,1.1.0,MIT -read-pkg,2.0.0,MIT -read-pkg-up,1.0.1,MIT -read-pkg-up,2.0.0,MIT -readable-stream,1.0.34,MIT -readable-stream,2.0.6,MIT -readable-stream,2.2.9,MIT -readable-stream,2.3.3,MIT -readdirp,2.1.0,MIT -readline2,1.0.1,MIT recaptcha,3.0.0,MIT -rechoir,0.6.2,MIT recursive-open-struct,1.0.0,MIT -recursive-readdir,2.1.1,MIT redcarpet,3.4.0,MIT -redent,1.0.0,MIT redis,3.3.3,MIT -redis-actionpack,5.0.1,MIT -redis-activesupport,5.0.1,MIT +redis-actionpack,5.0.2,MIT +redis-activesupport,5.0.4,MIT redis-namespace,1.5.2,MIT -redis-rack,1.6.0,MIT -redis-rails,5.0.1,MIT -redis-store,1.2.0,MIT -reduce-css-calc,1.3.0,MIT -reduce-function-call,1.0.2,MIT -regenerate,1.3.2,MIT -regenerator-runtime,0.10.1,MIT -regenerator-transform,0.9.8,BSD -regex-cache,0.4.3,MIT -regexpu-core,1.0.0,MIT -regexpu-core,2.0.0,MIT -registry-url,3.1.0,MIT -regjsgen,0.2.0,MIT -regjsparser,0.1.5,BSD -remove-trailing-separator,1.1.0,ISC -repeat-element,1.1.2,MIT -repeat-string,0.2.2,MIT -repeat-string,1.6.1,MIT -repeating,1.1.3,MIT -repeating,2.0.1,MIT +redis-rack,2.0.3,MIT +redis-rails,5.0.2,MIT +redis-store,1.4.1,MIT representable,3.0.4,MIT -request,2.81.0,Apache 2.0 request_store,1.3.1,MIT -require-directory,2.1.1,MIT -require-from-string,1.2.1,MIT -require-main-filename,1.0.1,ISC -require-uncached,1.0.3,MIT -requires-port,1.0.0,MIT -resolve,1.1.7,MIT -resolve,1.2.0,MIT -resolve-from,1.0.1,MIT responders,2.3.0,MIT rest-client,2.0.0,MIT -restore-cursor,1.0.1,MIT retriable,3.1.1,MIT -right-align,0.1.3,MIT -rimraf,2.6.1,ISC rinku,2.0.0,ISC -ripemd160,1.0.1,New BSD rotp,2.1.2,MIT rouge,2.2.1,MIT rqrcode,0.7.0,MIT @@ -1112,244 +262,51 @@ rubyntlm,0.6.2,MIT rubypants,0.2.0,BSD rufus-scheduler,3.4.0,MIT rugged,0.26.0,MIT -run-async,0.1.0,MIT -rx-lite,3.1.2,Apache 2.0 -safe-buffer,5.0.1,MIT -safe-buffer,5.1.1,MIT safe_yaml,1.0.4,MIT sanitize,2.1.0,MIT sass,3.4.22,MIT sass-rails,5.0.6,MIT sawyer,0.8.1,MIT -sax,1.2.2,ISC securecompare,1.0.0,MIT seed-fu,2.3.6,MIT -select,1.1.2,MIT -select-hose,2.0.0,MIT -select2,3.5.2-browserify,unknown select2-rails,3.5.9.3,MIT -selfsigned,1.10.1,MIT -semver,4.3.6,ISC -semver,5.3.0,ISC -semver-diff,2.1.0,MIT -send,0.15.4,MIT sentry-raven,2.5.3,Apache 2.0 -serve-index,1.9.0,MIT -serve-static,1.12.4,MIT -set-blocking,2.0.0,ISC -set-immediate-shim,1.0.1,MIT -setimmediate,1.0.5,MIT -setprototypeof,1.0.3,ISC settingslogic,2.0.9,MIT sexp_processor,4.9.0,MIT -sha.js,2.4.8,MIT -shebang-command,1.2.0,MIT -shebang-regex,1.0.0,MIT -shelljs,0.7.8,New BSD sidekiq,5.0.4,LGPL sidekiq-cron,0.6.0,MIT sidekiq-limit_fetch,3.4.0,MIT -sigmund,1.0.1,ISC -signal-exit,3.0.2,ISC signet,0.7.3,Apache 2.0 slack-notifier,1.5.1,MIT -slash,1.0.0,MIT -slice-ansi,0.0.4,MIT -slide,1.1.6,ISC -sntp,1.0.9,BSD -socket.io,1.7.3,MIT -socket.io-adapter,0.5.0,MIT -socket.io-client,1.7.3,MIT -socket.io-parser,2.3.1,MIT -sockjs,0.3.18,MIT -sockjs-client,1.0.1,MIT -sockjs-client,1.1.4,MIT -sort-keys,1.1.2,MIT -source-list-map,0.1.8,MIT -source-list-map,2.0.0,MIT -source-map,0.1.43,BSD -source-map,0.2.0,BSD -source-map,0.4.4,New BSD -source-map,0.5.6,New BSD -source-map-support,0.4.11,MIT -spdx-correct,1.0.2,Apache 2.0 -spdx-expression-parse,1.0.4,(MIT AND CC-BY-3.0) -spdx-license-ids,1.2.2,Unlicense -spdy,3.4.7,MIT -spdy-transport,2.0.20,MIT -split,0.3.3,MIT -sprintf-js,1.0.3,New BSD sprockets,3.7.1,MIT -sprockets-rails,3.2.0,MIT -sql.js,0.4.0,MIT -sshpk,1.13.0,MIT +sprockets-rails,3.2.1,MIT state_machines,0.4.0,MIT state_machines-activemodel,0.4.0,MIT state_machines-activerecord,0.4.0,MIT -statuses,1.3.1,MIT -stream-browserify,2.0.1,MIT -stream-combiner,0.0.4,MIT -stream-http,2.6.3,MIT -stream-shift,1.0.0,MIT -strict-uri-encode,1.1.0,MIT -string-length,1.0.1,MIT -string-width,1.0.2,MIT -string-width,2.0.0,MIT -string_decoder,0.10.31,MIT -string_decoder,1.0.1,MIT -string_decoder,1.0.3,MIT stringex,2.7.1,MIT -stringstream,0.0.5,MIT -strip-ansi,3.0.1,MIT -strip-bom,2.0.0,MIT -strip-bom,3.0.0,MIT -strip-eof,1.0.0,MIT -strip-indent,1.0.1,MIT -strip-json-comments,2.0.1,MIT -supports-color,2.0.0,MIT -supports-color,3.2.3,MIT -supports-color,4.2.1,MIT -svg4everybody,2.1.9,CC0-1.0 -svgo,0.7.2,MIT sys-filesystem,1.1.6,Artistic 2.0 -table,3.8.3,New BSD -tapable,0.1.10,MIT -tapable,0.2.8,MIT -tar,2.2.1,ISC -tar-pack,3.4.0,Simplified BSD temple,0.7.7,MIT -test-exclude,4.0.0,ISC text,1.3.1,MIT -text-table,0.2.0,MIT thor,0.19.4,MIT thread_safe,0.3.6,Apache 2.0 -three,0.84.0,MIT -three-orbit-controls,82.1.0,MIT -three-stl-loader,1.0.4,MIT -through,2.3.8,MIT -thunky,0.1.0,unknown tilt,2.0.6,MIT -timeago.js,2.0.5,MIT -timed-out,2.0.0,MIT -timed-out,4.0.1,MIT -timers-browserify,1.4.2,MIT -timers-browserify,2.0.4,MIT timfel-krb5-auth,0.8.3,LGPL -tiny-emitter,1.1.0,MIT -tmp,0.0.31,MIT -to-array,0.1.4,MIT -to-arraybuffer,1.0.1,MIT -to-fast-properties,1.0.2,MIT toml-rb,0.3.15,MIT -touch,1.0.0,ISC -tough-cookie,2.3.2,New BSD -traverse,0.6.6,MIT -trim-newlines,1.0.0,MIT -trim-right,1.0.1,MIT truncato,0.7.10,MIT -tryit,1.0.3,MIT -ts-loader,3.1.1,MIT -tty-browserify,0.0.0,MIT -tunnel-agent,0.6.0,Apache 2.0 -tweetnacl,0.14.5,Unlicense -type-check,0.3.2,MIT -type-is,1.6.15,MIT -typedarray,0.0.6,MIT -typescript,2.6.1,Apache 2.0 -tzinfo,1.2.3,MIT +tzinfo,1.2.4,MIT u2f,0.2.1,MIT uber,0.1.0,MIT uglifier,2.7.2,MIT -uglify-js,2.8.29,Simplified BSD -uglify-to-browserify,1.0.2,MIT -uglifyjs-webpack-plugin,0.4.6,MIT -uid-number,0.0.6,ISC -ultron,1.0.2,MIT -ultron,1.1.0,MIT -unc-path-regex,0.1.2,MIT -undefsafe,0.0.3,MIT / http://rem.mit-license.org -underscore,1.8.3,MIT unf,0.1.4,BSD unf_ext,0.0.7.4,MIT unicorn,5.1.0,ruby unicorn-worker-killer,0.4.4,ruby -uniq,1.0.1,MIT -uniqid,4.1.1,MIT -uniqs,2.0.0,MIT -unpipe,1.0.0,MIT -update-notifier,0.5.0,Simplified BSD -url,0.11.0,MIT -url-loader,0.5.8,MIT -url-parse,1.0.5,MIT -url-parse,1.1.7,MIT -url-parse,1.1.9,MIT -url-parse-lax,1.0.0,MIT -url-to-options,1.0.1,MIT url_safe_base64,0.2.2,MIT -user-home,2.0.0,MIT -useragent,2.2.1,MIT -util,0.10.3,MIT -util-deprecate,1.0.2,MIT -utils-merge,1.0.0,MIT -uuid,2.0.3,MIT -uuid,3.0.1,MIT -validate-npm-package-license,3.0.1,Apache 2.0 validates_hostname,1.0.6,MIT -vary,1.1.1,MIT -vendors,1.0.1,MIT -verror,1.3.6,MIT version_sorter,2.1.0,MIT virtus,1.0.5,MIT -visibilityjs,1.2.4,MIT -vm-browserify,0.0.4,MIT vmstat,2.3.0,MIT -void-elements,2.0.1,MIT -vue,2.5.2,MIT -vue-hot-reload-api,2.0.11,MIT -vue-loader,11.3.4,MIT -vue-resource,1.3.4,MIT -vue-style-loader,2.0.5,MIT -vue-template-compiler,2.5.2,MIT -vue-template-es2015-compiler,1.5.1,MIT -vuex,3.0.0,MIT warden,1.2.6,MIT -watchpack,1.4.0,MIT -wbuf,1.7.2,MIT -webpack,3.5.5,MIT -webpack-bundle-analyzer,2.8.2,MIT -webpack-dev-middleware,1.11.0,MIT -webpack-dev-server,2.7.1,MIT webpack-rails,0.9.10,MIT -webpack-sources,1.0.1,MIT -webpack-stats-plugin,0.1.5,MIT -websocket-driver,0.6.5,MIT -websocket-extensions,0.1.1,MIT -whet.extend,0.9.9,MIT -which,1.2.12,ISC -which-module,1.0.0,ISC -which-module,2.0.0,ISC -wide-align,1.1.2,ISC wikicloth,0.8.1,MIT -window-size,0.1.0,MIT -wordwrap,0.0.2,MIT/X11 -wordwrap,0.0.3,MIT -wordwrap,1.0.0,MIT -wrap-ansi,2.1.0,MIT -wrappy,1.0.2,ISC -write,0.2.1,MIT -write-file-atomic,1.3.4,ISC -ws,1.1.2,MIT -ws,2.3.1,MIT -wtf-8,1.0.0,MIT -xdg-basedir,2.0.0,MIT xml-simple,1.1.5,ruby -xmlhttprequest-ssl,1.5.3,MIT -xtend,4.0.1,MIT -y18n,3.2.1,ISC -yallist,2.1.2,ISC -yargs,3.10.0,MIT -yargs,6.6.0,MIT -yargs,8.0.2,MIT -yargs-parser,4.2.1,ISC -yargs-parser,7.0.0,ISC -yeast,0.1.2,MIT |