diff options
70 files changed, 693 insertions, 420 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b69c5ee14c..b716dd4bf13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.3.2 (2017-12-28) + +### Fixed (1 change) + +- Fix migration for removing orphaned issues.moved_to_id values in MySQL and PostgreSQL. + + ## 10.3.1 (2017-12-27) ### Fixed (3 changes) @@ -12,7 +12,7 @@ gem 'sprockets', '~> 3.7.0' gem 'default_value_for', '~> 3.0.0' # Supported DBs -gem 'mysql2', '~> 0.4.5', group: :mysql +gem 'mysql2', '~> 0.4.10', group: :mysql gem 'pg', '~> 0.18.2', group: :postgres gem 'rugged', '~> 0.26.0' @@ -283,7 +283,7 @@ group :metrics do gem 'influxdb', '~> 0.2', require: false # Prometheus - gem 'prometheus-client-mmap', '~> 0.7.0.beta43' + gem 'prometheus-client-mmap', '~> 0.7.0.beta44' gem 'raindrops', '~> 0.18' end diff --git a/Gemfile.lock b/Gemfile.lock index 0b714b6fe5d..3cffed4901b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -505,7 +505,7 @@ GEM mustermann (1.0.0) mustermann-grape (1.0.0) mustermann (~> 1.0.0) - mysql2 (0.4.5) + mysql2 (0.4.10) net-ldap (0.16.0) net-ssh (4.1.0) netrc (0.11.0) @@ -634,7 +634,7 @@ GEM parser unparser procto (0.0.3) - prometheus-client-mmap (0.7.0.beta43) + prometheus-client-mmap (0.7.0.beta44) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -708,7 +708,7 @@ GEM json recursive-open-struct (1.0.0) redcarpet (3.4.0) - redis (3.3.3) + redis (3.3.5) redis-actionpack (5.0.2) actionpack (>= 4.0, < 6) redis-rack (>= 1, < 3) @@ -839,11 +839,11 @@ GEM rack shoulda-matchers (3.1.2) activesupport (>= 4.0.0) - sidekiq (5.0.4) + sidekiq (5.0.5) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) - redis (~> 3.3, >= 3.3.3) + redis (>= 3.3.4, < 5) sidekiq-cron (0.6.0) rufus-scheduler (>= 3.3.0) sidekiq (>= 4.2.1) @@ -1087,7 +1087,7 @@ DEPENDENCIES method_source (~> 0.8) minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) - mysql2 (~> 0.4.5) + mysql2 (~> 0.4.10) net-ldap net-ssh (~> 4.1.0) nokogiri (~> 1.8.1) @@ -1122,7 +1122,7 @@ DEPENDENCIES peek-sidekiq (~> 1.0.3) pg (~> 0.18.2) premailer-rails (~> 1.9.7) - prometheus-client-mmap (~> 0.7.0.beta43) + prometheus-client-mmap (~> 0.7.0.beta44) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 20d23162940..0c1cff1da7a 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -102,7 +102,6 @@ $(() => { if (list.type === 'closed') { list.position = Infinity; - list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' }; } else if (list.type === 'backlog') { list.position = -1; } diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 616de2347e1..983429550f0 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -1,5 +1,4 @@ /* eslint-disable comma-dangle, space-before-function-paren, no-new */ -/* global MilestoneSelect */ import Vue from 'vue'; import Flash from '../../flash'; @@ -12,6 +11,7 @@ import './sidebar/remove_issue'; import IssuableContext from '../../issuable_context'; import LabelsSelect from '../../labels_select'; import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue'; +import MilestoneSelect from '../../milestone_select'; const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 118437b82a3..42f61d33f6e 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -5,7 +5,7 @@ import IssuableIndex from './issuable_index'; import Milestone from './milestone'; import IssuableForm from './issuable_form'; import LabelsSelect from './labels_select'; -/* global MilestoneSelect */ +import MilestoneSelect from './milestone_select'; import NewBranchForm from './new_branch_form'; import NotificationsForm from './notifications_form'; import notificationsDropdown from './notifications_dropdown'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 2ba85c7da97..c05a83176f2 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -127,7 +127,7 @@ class FilteredSearchManager { this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this); this.onClearSearchWrapper = this.onClearSearch.bind(this); - this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + this.checkForBackspaceWrapper = this.checkForBackspace.call(this); this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this); this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); this.editTokenWrapper = this.editToken.bind(this); @@ -180,22 +180,34 @@ class FilteredSearchManager { this.unbindStateEvents(); } - checkForBackspace(e) { - // 8 = Backspace Key - // 46 = Delete Key - if (e.keyCode === 8 || e.keyCode === 46) { - const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + checkForBackspace() { + let backspaceCount = 0; + + // closure for keeping track of the number of backspace keystrokes + return (e) => { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { + const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken); + const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue); + + if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { + backspaceCount += 1; + + if (backspaceCount === 2) { + backspaceCount = 0; + this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + } + } - const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken); - const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue); - if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { - this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); - gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + // Reposition dropdown so that it is aligned with cursor + this.dropdownManager.updateCurrentDropdownOffset(); + } else { + backspaceCount = 0; } - - // Reposition dropdown so that it is aligned with cursor - this.dropdownManager.updateCurrentDropdownOffset(); - } + }; } checkForEnter(e) { diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js index 5d4c1851fe5..e61b37a2d1f 100644 --- a/app/assets/javascripts/init_issuable_sidebar.js +++ b/app/assets/javascripts/init_issuable_sidebar.js @@ -1,5 +1,6 @@ /* eslint-disable no-new */ -/* global MilestoneSelect */ + +import MilestoneSelect from './milestone_select'; import LabelsSelect from './labels_select'; import IssuableContext from './issuable_context'; import Sidebar from './right_sidebar'; diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js index 2cbb70220d0..b6ff97d1279 100644 --- a/app/assets/javascripts/init_legacy_filters.js +++ b/app/assets/javascripts/init_legacy_filters.js @@ -1,9 +1,9 @@ /* eslint-disable no-new */ import LabelsSelect from './labels_select'; -/* global MilestoneSelect */ import subscriptionSelect from './subscription_select'; import UsersSelect from './users_select'; import issueStatusSelect from './issue_status_select'; +import MilestoneSelect from './milestone_select'; export default () => { new UsersSelect(); diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index bf77b93b643..2056efe701b 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -1,8 +1,7 @@ /* eslint-disable class-methods-use-this, no-new */ -/* global MilestoneSelect */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; -import './milestone_select'; +import MilestoneSelect from './milestone_select'; import issueStatusSelect from './issue_status_select'; import subscriptionSelect from './subscription_select'; import LabelsSelect from './labels_select'; diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 2e5e818d61d..0e854295fe3 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,237 +1,228 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ /* global Issuable */ /* global ListMilestone */ import _ from 'underscore'; import { timeFor } from './lib/utils/datetime_utility'; -(function() { - this.MilestoneSelect = (function() { - function MilestoneSelect(currentProject, els, options = {}) { - var _this, $els; - if (currentProject != null) { - _this = this; - this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; - } +export default class MilestoneSelect { + constructor(currentProject, els, options = {}) { + if (currentProject !== null) { + this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; + } - $els = $(els); + this.init(els, options); + } - if (!els) { - $els = $('.js-milestone-select'); - } + init(els, options) { + let $els = $(els); - $els.each(function(i, dropdown) { - var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, defaultNo, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, selectedMilestoneDefault, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove; - $dropdown = $(dropdown); - projectId = $dropdown.data('project-id'); - milestonesUrl = $dropdown.data('milestones'); - issueUpdateURL = $dropdown.data('issueUpdate'); - showNo = $dropdown.data('show-no'); - showAny = $dropdown.data('show-any'); - showMenuAbove = $dropdown.data('showMenuAbove'); - showUpcoming = $dropdown.data('show-upcoming'); - showStarted = $dropdown.data('show-started'); - useId = $dropdown.data('use-id'); - defaultLabel = $dropdown.data('default-label'); - defaultNo = $dropdown.data('default-no'); - issuableId = $dropdown.data('issuable-id'); - abilityName = $dropdown.data('ability-name'); - $selectbox = $dropdown.closest('.selectbox'); - $block = $selectbox.closest('.block'); - $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); - $value = $block.find('.value'); - $loading = $block.find('.block-loading').fadeOut(); - selectedMilestoneDefault = (showAny ? '' : null); - selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault); - selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault; - if (issueUpdateURL) { - milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); - milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; - collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>'); - } - return $dropdown.glDropdown({ - showMenuAbove: showMenuAbove, - data: function(term, callback) { - return $.ajax({ - url: milestonesUrl - }).done(function(data) { - var extraOptions = []; - if (showAny) { - extraOptions.push({ - id: 0, - name: '', - title: 'Any Milestone' - }); - } - if (showNo) { - extraOptions.push({ - id: -1, - name: 'No Milestone', - title: 'No Milestone' - }); - } - if (showUpcoming) { - extraOptions.push({ - id: -2, - name: '#upcoming', - title: 'Upcoming' - }); - } - if (showStarted) { - extraOptions.push({ - id: -3, - name: '#started', - title: 'Started' - }); - } - if (extraOptions.length) { - extraOptions.push('divider'); - } + if (!els) { + $els = $('.js-milestone-select'); + } - callback(extraOptions.concat(data)); - if (showMenuAbove) { - $dropdown.data('glDropdown').positionMenuAbove(); - } - $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); - }); - }, - renderRow: function(milestone) { - return ` - <li data-milestone-id="${milestone.name}"> - <a href='#' class='dropdown-menu-milestone-link'> - ${_.escape(milestone.title)} - </a> - </li> - `; - }, - filterable: true, - search: { - fields: ['title'] - }, - selectable: true, - toggleLabel: function(selected, el, e) { - if (selected && 'id' in selected && $(el).hasClass('is-active')) { - return selected.title; - } else { - return defaultLabel; - } - }, - defaultLabel: defaultLabel, - fieldName: $dropdown.data('field-name'), - text: function(milestone) { - return _.escape(milestone.title); - }, - id: function(milestone) { - if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { - return milestone.name; - } else { - return milestone.id; - } - }, - isSelected: function(milestone) { - return milestone.name === selectedMilestone; - }, - hidden: function() { - $selectbox.hide(); - // display:block overrides the hide-collapse rule - return $value.css('display', ''); - }, - opened: function(e) { - const $el = $(e.currentTarget); - if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) { - selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; - } - $('a.is-active', $el).removeClass('is-active'); - $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); - }, - vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(clickEvent) { - const { $el, e } = clickEvent; - let selected = clickEvent.selectedObj; + $els.each((i, dropdown) => { + let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault; + const $dropdown = $(dropdown); + const projectId = $dropdown.data('project-id'); + const milestonesUrl = $dropdown.data('milestones'); + const issueUpdateURL = $dropdown.data('issueUpdate'); + const showNo = $dropdown.data('show-no'); + const showAny = $dropdown.data('show-any'); + const showMenuAbove = $dropdown.data('showMenuAbove'); + const showUpcoming = $dropdown.data('show-upcoming'); + const showStarted = $dropdown.data('show-started'); + const useId = $dropdown.data('use-id'); + const defaultLabel = $dropdown.data('default-label'); + const defaultNo = $dropdown.data('default-no'); + const issuableId = $dropdown.data('issuable-id'); + const abilityName = $dropdown.data('ability-name'); + const $selectBox = $dropdown.closest('.selectbox'); + const $block = $selectBox.closest('.block'); + const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); + const $value = $block.find('.value'); + const $loading = $block.find('.block-loading').fadeOut(); + selectedMilestoneDefault = (showAny ? '' : null); + selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault); + selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault; - var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; - if (!selected) return; + if (issueUpdateURL) { + milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); + milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; + collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>'); + } + return $dropdown.glDropdown({ + showMenuAbove: showMenuAbove, + data: (term, callback) => $.ajax({ + url: milestonesUrl + }).done((data) => { + const extraOptions = []; + if (showAny) { + extraOptions.push({ + id: 0, + name: '', + title: 'Any Milestone' + }); + } + if (showNo) { + extraOptions.push({ + id: -1, + name: 'No Milestone', + title: 'No Milestone' + }); + } + if (showUpcoming) { + extraOptions.push({ + id: -2, + name: '#upcoming', + title: 'Upcoming' + }); + } + if (showStarted) { + extraOptions.push({ + id: -3, + name: '#started', + title: 'Started' + }); + } + if (extraOptions.length) { + extraOptions.push('divider'); + } - if (options.handleClick) { - e.preventDefault(); - options.handleClick(selected); - return; - } + callback(extraOptions.concat(data)); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); + } + $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); + }), + renderRow: milestone => ` + <li data-milestone-id="${milestone.name}"> + <a href='#' class='dropdown-menu-milestone-link'> + ${_.escape(milestone.title)} + </a> + </li> + `, + filterable: true, + search: { + fields: ['title'] + }, + selectable: true, + toggleLabel: (selected, el, e) => { + if (selected && 'id' in selected && $(el).hasClass('is-active')) { + return selected.title; + } else { + return defaultLabel; + } + }, + defaultLabel: defaultLabel, + fieldName: $dropdown.data('field-name'), + text: milestone => _.escape(milestone.title), + id: (milestone) => { + if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { + return milestone.name; + } else { + return milestone.id; + } + }, + isSelected: milestone => milestone.name === selectedMilestone, + hidden: () => { + $selectBox.hide(); + // display:block overrides the hide-collapse rule + return $value.css('display', ''); + }, + opened: (e) => { + const $el = $(e.currentTarget); + if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) { + selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; + } + $('a.is-active', $el).removeClass('is-active'); + $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); + }, + vue: $dropdown.hasClass('js-issue-board-sidebar'), + clicked: (clickEvent) => { + const { $el, e } = clickEvent; + let selected = clickEvent.selectedObj; - page = $('body').attr('data-page'); - isIssueIndex = page === 'projects:issues:index'; - isMRIndex = (page === page && page === 'projects:merge_requests:index'); - isSelecting = (selected.name !== selectedMilestone); - selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault; - if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { - e.preventDefault(); - return; - } + let data, boardsStore; + if (!selected) return; - if ($dropdown.closest('.add-issues-modal').length) { - boardsStore = gl.issueBoards.ModalStore.store.filter; - } + if (options.handleClick) { + e.preventDefault(); + options.handleClick(selected); + return; + } - if (boardsStore) { - boardsStore[$dropdown.data('field-name')] = selected.name; - e.preventDefault(); - } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { - return Issuable.filterResults($dropdown.closest('form')); - } else if ($dropdown.hasClass('js-filter-submit')) { - return $dropdown.closest('form').submit(); - } else if ($dropdown.hasClass('js-issue-board-sidebar')) { - if (selected.id !== -1 && isSelecting) { - gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({ - id: selected.id, - title: selected.name - })); - } else { - gl.issueBoards.boardStoreIssueDelete('milestone'); - } + const page = $('body').attr('data-page'); + const isIssueIndex = page === 'projects:issues:index'; + const isMRIndex = (page === page && page === 'projects:merge_requests:index'); + const isSelecting = (selected.name !== selectedMilestone); + selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault; + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + e.preventDefault(); + return; + } - $dropdown.trigger('loading.gl.dropdown'); - $loading.removeClass('hidden').fadeIn(); + if ($dropdown.closest('.add-issues-modal').length) { + boardsStore = gl.issueBoards.ModalStore.store.filter; + } - gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) - .then(function () { - $dropdown.trigger('loaded.gl.dropdown'); - $loading.fadeOut(); - }) - .catch(() => { - $loading.fadeOut(); - }); + if (boardsStore) { + boardsStore[$dropdown.data('field-name')] = selected.name; + e.preventDefault(); + } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + return Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } else if ($dropdown.hasClass('js-issue-board-sidebar')) { + if (selected.id !== -1 && isSelecting) { + gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({ + id: selected.id, + title: selected.name + })); } else { - selected = $selectbox.find('input[type="hidden"]').val(); - data = {}; - data[abilityName] = {}; - data[abilityName].milestone_id = selected != null ? selected : null; - $loading.removeClass('hidden').fadeIn(); - $dropdown.trigger('loading.gl.dropdown'); - return $.ajax({ - type: 'PUT', - url: issueUpdateURL, - data: data - }).done(function(data) { + gl.issueBoards.boardStoreIssueDelete('milestone'); + } + + $dropdown.trigger('loading.gl.dropdown'); + $loading.removeClass('hidden').fadeIn(); + + gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) + .then(() => { $dropdown.trigger('loaded.gl.dropdown'); $loading.fadeOut(); - $selectbox.hide(); - $value.css('display', ''); - if (data.milestone != null) { - data.milestone.full_path = _this.currentProject.full_path; - 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)); - } else { - $value.html(milestoneLinkNoneTemplate); - return $sidebarCollapsedValue.find('span').text('No'); - } + }) + .catch(() => { + $loading.fadeOut(); }); - } + } else { + selected = $selectBox.find('input[type="hidden"]').val(); + data = {}; + data[abilityName] = {}; + data[abilityName].milestone_id = selected != null ? selected : null; + $loading.removeClass('hidden').fadeIn(); + $dropdown.trigger('loading.gl.dropdown'); + return $.ajax({ + type: 'PUT', + url: issueUpdateURL, + data: data + }).done((data) => { + $dropdown.trigger('loaded.gl.dropdown'); + $loading.fadeOut(); + $selectBox.hide(); + $value.css('display', ''); + if (data.milestone != null) { + data.milestone.full_path = this.currentProject.full_path; + 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)); + } else { + $value.html(milestoneLinkNoneTemplate); + return $sidebarCollapsedValue.find('span').text('No'); + } + }); } - }); + } }); - } - - return MilestoneSelect; - })(); -}).call(window); + }); + } +} diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 9f9773575a5..c950d0f7001 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -29,17 +29,17 @@ class Projects::RunnersController < Projects::ApplicationController def resume if Ci::UpdateRunnerService.new(@runner).update(active: true) - redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' + redirect_to runners_path(@project), notice: 'Runner was successfully updated.' else - redirect_to runner_path(@runner), alert: 'Runner was not updated.' + redirect_to runners_path(@project), alert: 'Runner was not updated.' end end def pause if Ci::UpdateRunnerService.new(@runner).update(active: false) - redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' + redirect_to runners_path(@project), notice: 'Runner was successfully updated.' else - redirect_to runner_path(@runner), alert: 'Runner was not updated.' + redirect_to runners_path(@project), alert: 'Runner was not updated.' end end diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb index d67b16584a4..bd6af622bfb 100644 --- a/app/models/diff_discussion.rb +++ b/app/models/diff_discussion.rb @@ -23,8 +23,13 @@ class DiffDiscussion < Discussion def merge_request_version_params return unless for_merge_request? + version_params = get_params + + return version_params unless on_merge_request_commit? && commit_id + + version_params ||= {} version_params.tap do |params| - params[:commit_id] = commit_id if on_merge_request_commit? + params[:commit_id] = commit_id end end @@ -37,7 +42,7 @@ class DiffDiscussion < Discussion private - def version_params + def get_params return {} if active? noteable.version_params_for(position.diff_refs) diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 03be7039b2a..348eb0bf8d8 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -26,7 +26,7 @@ module Projects name: @project.name, path: @project.path, shared_runners_enabled: @project.shared_runners_enabled, - namespace_id: @params[:namespace].try(:id) || current_user.namespace.id + namespace_id: target_namespace.id } if @project.avatar.present? && @project.avatar.image? @@ -74,14 +74,14 @@ module Projects Projects::ForksCountService.new(@project).refresh_cache end + def target_namespace + @target_namespace ||= @params[:namespace] || current_user.namespace + end + def allowed_visibility_level - project_level = @project.visibility_level + target_level = [@project.visibility_level, target_namespace.visibility_level].min - if Gitlab::VisibilityLevel.non_restricted_level?(project_level) - project_level - else - Gitlab::VisibilityLevel.highest_allowed_level - end + Gitlab::VisibilityLevel.closest_allowed_level(target_level) end end end diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index 72eab964766..6364f0be4a3 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -1,3 +1,5 @@ +- add_to_breadcrumbs "Applications", oauth_applications_path +- breadcrumb_title @application.name - page_title @application.name, "Applications" - @content_class = "limit-container-width" unless fluid_layout diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 16038ef2f79..76a8099d7c0 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -13,13 +13,13 @@ = group_icon(@group, alt: '', class: 'avatar group-avatar s160') %p.light - if @group.avatar? - You can change your group avatar here + You can change the group avatar here - else You can upload a group avatar here = render 'shared/choose_group_avatar_button', f: f - if @group.avatar? %hr - = link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" + = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _("Avatar will be removed. Are you sure?")}, method: :delete, class: "btn btn-danger btn-inverted" = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index cb8db306b56..dd086f70641 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -130,7 +130,7 @@ %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) %ul.sidebar-sub-level-items.is-fly-out-only = nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do - = link_to admin_broadcast_messages_path do + = link_to admin_abuse_reports_path do %strong.fly-out-top-item-name #{ _('Abuse Reports') } %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all)) diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml index 1a392e29e2a..cbea5ca605a 100644 --- a/app/views/profiles/audit_log.html.haml +++ b/app/views/profiles/audit_log.html.haml @@ -4,7 +4,7 @@ .row.prepend-top-default .col-lg-4.profile-settings-sidebar - %h3.prepend-top-0 + %h4.prepend-top-0 = page_title %p This is a security log of important events involving your account. diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml index 8dbb8aef31b..86ebec0179c 100644 --- a/app/views/profiles/gpg_keys/index.html.haml +++ b/app/views/profiles/gpg_keys/index.html.haml @@ -1,4 +1,5 @@ - page_title "GPG Keys" +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' .row.prepend-top-default diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml index 172c0450381..7b7960708c4 100644 --- a/app/views/profiles/keys/show.html.haml +++ b/app/views/profiles/keys/show.html.haml @@ -1,3 +1,5 @@ +- add_to_breadcrumbs "SSH Keys", profile_keys_path +- breadcrumb_title @key.title - page_title @key.title, "SSH Keys" - @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index e98fdfc7a3d..202eccb7bb6 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -10,16 +10,16 @@ %li= msg = hidden_field_tag :notification_type, 'global' - .row + .row.prepend-top-default .col-lg-4.profile-settings-sidebar - %h4 + %h4.prepend-top-0 = page_title %p You can specify notification level per group or per project. %p By default, all projects and groups will use the global notifications setting. .col-lg-8 - %h5 + %h5.prepend-top-0 Global notification settings = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 79f334176a5..0f773933ac2 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -22,18 +22,15 @@ .clearfix.avatar-image.append-bottom-default = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' - %h5.prepend-top-0 - Upload new avatar + %h5.prepend-top-0= _("Upload new avatar") .prepend-top-5.append-bottom-10 - %a.btn.js-choose-user-avatar-button - Browse file... - %span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen + %button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...") + %span.avatar-file-name.prepend-left-default.js-avatar-filename= _("No file chosen") = f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*' - .help-block - The maximum file size allowed is 200KB. + .help-block= _("The maximum file size allowed is 200KB.") - if @user.avatar? %hr - = link_to 'Remove avatar', profile_avatar_path, data: { confirm: 'Avatar will be removed. Are you sure?' }, method: :delete, class: 'btn btn-gray' + = link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted' %hr .row .col-lg-4.profile-settings-sidebar diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 71206f3a386..e16d132f869 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -42,24 +42,23 @@ = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" %p.help-block Separate tags with commas. %fieldset.features - %h5.prepend-top-0 - Project avatar + %h5.prepend-top-0= _("Project avatar") .form-group - if @project.avatar? - .avatar-container.s160 + .avatar-container.s160.append-bottom-15 = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160') - %p.light - - if @project.avatar_in_git - Project avatar in repository: #{ @project.avatar_in_git } - %a.choose-btn.btn.js-choose-project-avatar-button - Browse file... - %span.file_name.prepend-left-default.js-avatar-filename No file chosen - = f.file_field :avatar, class: "js-project-avatar-input hidden" - .help-block The maximum file size allowed is 200KB. + - if @project.avatar_in_git + %p.light + = _("Project avatar in repository: %{link}").html_safe % { link: @project.avatar_in_git } + .prepend-top-5.append-bottom-10 + %button.btn.js-choose-project-avatar-button{ type: 'button' }= _("Choose file...") + %span.file_name.prepend-left-default.js-avatar-filename= _("No file chosen") + = f.file_field :avatar, class: "js-project-avatar-input hidden" + .help-block= _("The maximum file size allowed is 200KB.") - if @project.avatar? %hr - = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - = f.submit 'Save changes', class: "btn btn-save" + = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted" + = f.submit 'Save changes', class: "btn btn-success" %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 25d862ab4de..6376496ee1a 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -17,6 +17,10 @@ .pull-right - if @project_runners.include?(runner) + - if runner.active? + = link_to 'Pause', pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: "Are you sure?" } + - else + = link_to 'Resume', resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm' - if runner.belongs_to_one_project? = link_to 'Remove Runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - else diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml index 94295970acf..75c65520350 100644 --- a/app/views/shared/_choose_group_avatar_button.html.haml +++ b/app/views/shared/_choose_group_avatar_button.html.haml @@ -1,7 +1,4 @@ -%button.choose-btn.btn.btn-sm.js-choose-group-avatar-button{ type: 'button' } - %i.fa.fa-paperclip - %span Choose File ... - -%span.file_name.js-avatar-filename File name... -= f.file_field :avatar, class: 'js-group-avatar-input hidden' -.light The maximum file size allowed is 200KB. +%button.btn.js-choose-group-avatar-button{ type: 'button' }= _("Choose File ...") +%span.file_name.js-avatar-filename= _("No file chosen") += f.file_field :avatar, class: "js-group-avatar-input hidden" +.help-block= _("The maximum file size allowed is 200KB.") diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml index 0e816870f15..93a4301f366 100644 --- a/app/views/shared/_recaptcha_form.html.haml +++ b/app/views/shared/_recaptcha_form.html.haml @@ -1,9 +1,10 @@ - resource_name = spammable.class.model_name.singular - humanized_resource_name = spammable.class.model_name.human.downcase - script = local_assigns.fetch(:script, true) +- method = params[:action] == 'create' ? :post : :put - has_submit = local_assigns.fetch(:has_submit, true) -= form_for resource_name, method: :post, html: { class: 'recaptcha-form js-recaptcha-form' } do |f| += form_for resource_name, method: method, html: { class: 'recaptcha-form js-recaptcha-form' } do |f| .recaptcha - params[resource_name].each do |field, value| = hidden_field(resource_name, field, value: value) diff --git a/changelogs/unreleased/16036-ignore-lost-found-folder-during-backup-on-a-volume.yml b/changelogs/unreleased/16036-ignore-lost-found-folder-during-backup-on-a-volume.yml new file mode 100644 index 00000000000..833650559a3 --- /dev/null +++ b/changelogs/unreleased/16036-ignore-lost-found-folder-during-backup-on-a-volume.yml @@ -0,0 +1,5 @@ +--- +title: "Ignore lost+found folder during backup on a volume" +merge_request: 16036 +author: Julien Millau +type: fixed
\ No newline at end of file diff --git a/changelogs/unreleased/20035-pause-resume-runners.yml b/changelogs/unreleased/20035-pause-resume-runners.yml new file mode 100644 index 00000000000..98757e60683 --- /dev/null +++ b/changelogs/unreleased/20035-pause-resume-runners.yml @@ -0,0 +1,5 @@ +--- +title: Add pause/resume button to project runners +merge_request: 16032 +author: Mario de la Ossa +type: added diff --git a/changelogs/unreleased/38596-fix-backspace-visual-token-clearing.yml b/changelogs/unreleased/38596-fix-backspace-visual-token-clearing.yml new file mode 100644 index 00000000000..4a9d0b66a8c --- /dev/null +++ b/changelogs/unreleased/38596-fix-backspace-visual-token-clearing.yml @@ -0,0 +1,5 @@ +--- +title: Clears visual token on second backspace +merge_request: +author: Martin Wortschack +type: fixed diff --git a/changelogs/unreleased/40274-user-settings-breadcrumbs.yml b/changelogs/unreleased/40274-user-settings-breadcrumbs.yml new file mode 100644 index 00000000000..1f381668aca --- /dev/null +++ b/changelogs/unreleased/40274-user-settings-breadcrumbs.yml @@ -0,0 +1,5 @@ +--- +title: Fix breadcrumbs in User Settings +merge_request: 16172 +author: rfwatson +type: fixed diff --git a/changelogs/unreleased/40780-choose-file.yml b/changelogs/unreleased/40780-choose-file.yml new file mode 100644 index 00000000000..73e59dfcce8 --- /dev/null +++ b/changelogs/unreleased/40780-choose-file.yml @@ -0,0 +1,5 @@ +--- +title: Update Browse file to Choose file in all occurences +merge_request: +author: +type: other diff --git a/changelogs/unreleased/41492-mr-comment-fix.yml b/changelogs/unreleased/41492-mr-comment-fix.yml new file mode 100644 index 00000000000..45ddc16aad0 --- /dev/null +++ b/changelogs/unreleased/41492-mr-comment-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fix links to old commits in merge request comments +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/bump_mysql_gem.yml b/changelogs/unreleased/bump_mysql_gem.yml new file mode 100644 index 00000000000..58166949d72 --- /dev/null +++ b/changelogs/unreleased/bump_mysql_gem.yml @@ -0,0 +1,5 @@ +--- +title: Bump mysql2 gem version from 0.4.5 to 0.4.10 +merge_request: +author: asaparov +type: other diff --git a/changelogs/unreleased/bvl-fork-public-project-to-private-namespace.yml b/changelogs/unreleased/bvl-fork-public-project-to-private-namespace.yml new file mode 100644 index 00000000000..b802625943d --- /dev/null +++ b/changelogs/unreleased/bvl-fork-public-project-to-private-namespace.yml @@ -0,0 +1,5 @@ +--- +title: Allow forking a public project to a private group +merge_request: 16050 +author: +type: changed diff --git a/changelogs/unreleased/fix-abuse-reports-link-url.yml b/changelogs/unreleased/fix-abuse-reports-link-url.yml new file mode 100644 index 00000000000..44c26f35984 --- /dev/null +++ b/changelogs/unreleased/fix-abuse-reports-link-url.yml @@ -0,0 +1,5 @@ +--- +title: Fix abuse reports link url in admin area navbar +merge_request: 16068 +author: megos +type: fixed diff --git a/changelogs/unreleased/fix-profile-settings-content-width.yml b/changelogs/unreleased/fix-profile-settings-content-width.yml new file mode 100644 index 00000000000..bf164dc587d --- /dev/null +++ b/changelogs/unreleased/fix-profile-settings-content-width.yml @@ -0,0 +1,5 @@ +--- +title: Adjust content width for User Settings, GPG Keys +merge_request: 16093 +author: George Tsiolis +type: fixed diff --git a/changelogs/unreleased/fix-profile-settings-sidebar-heading.yml b/changelogs/unreleased/fix-profile-settings-sidebar-heading.yml new file mode 100644 index 00000000000..75e0ea5612f --- /dev/null +++ b/changelogs/unreleased/fix-profile-settings-sidebar-heading.yml @@ -0,0 +1,5 @@ +--- +title: Keep typographic hierarchy in User Settings +merge_request: 16090 +author: George Tsiolis +type: fixed diff --git a/changelogs/unreleased/sh-fix-mysql-migration-10-3.yml b/changelogs/unreleased/sh-fix-mysql-migration-10-3.yml deleted file mode 100644 index d3d1d2f8256..00000000000 --- a/changelogs/unreleased/sh-fix-mysql-migration-10-3.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix migration for removing orphaned issues.moved_to_id values in MySQL and PostgreSQL -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-spam-update-404.yml b/changelogs/unreleased/sh-fix-spam-update-404.yml new file mode 100644 index 00000000000..13daec35ecf --- /dev/null +++ b/changelogs/unreleased/sh-fix-spam-update-404.yml @@ -0,0 +1,5 @@ +--- +title: Fix 404 errors after a user edits an issue description and solves the reCAPTCHA +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/show_proper_labels_in_board_issue_sidebar_when_issue_is_closed.yml b/changelogs/unreleased/show_proper_labels_in_board_issue_sidebar_when_issue_is_closed.yml new file mode 100644 index 00000000000..c2ab34b20a5 --- /dev/null +++ b/changelogs/unreleased/show_proper_labels_in_board_issue_sidebar_when_issue_is_closed.yml @@ -0,0 +1,5 @@ +--- +title: show None when issue is in closed list and no labels assigned +merge_request: 15976 +author: Christiaan Van den Poel +type: fixed diff --git a/config.ru b/config.ru index 065ce59932f..de0400f4f67 100644 --- a/config.ru +++ b/config.ru @@ -17,6 +17,11 @@ end require ::File.expand_path('../config/environment', __FILE__) +warmup do |app| + client = Rack::MockRequest.new(app) + client.get('/') +end + map ENV['RAILS_RELATIVE_URL_ROOT'] || "/" do run Gitlab::Application end diff --git a/config/initializers/active_record_data_types.rb b/config/initializers/active_record_data_types.rb index fef591c397d..0359e14b232 100644 --- a/config/initializers/active_record_data_types.rb +++ b/config/initializers/active_record_data_types.rb @@ -79,3 +79,8 @@ elsif Gitlab::Database.mysql? NATIVE_DATABASE_TYPES[:datetime_with_timezone] = { name: 'timestamp' } end end + +# Ensure `datetime_with_timezone` columns are correctly written to schema.rb +if (ActiveRecord::Base.connection.active? rescue false) + ActiveRecord::Base.connection.send :reload_type_map +end diff --git a/config/initializers/asset_sync.rb b/config/initializers/asset_sync.rb index db8500f6231..7f3934853fa 100644 --- a/config/initializers/asset_sync.rb +++ b/config/initializers/asset_sync.rb @@ -14,8 +14,8 @@ AssetSync.configure do |config| config.fog_directory = ENV['FOG_DIRECTORY'] if ENV.has_key?('FOG_DIRECTORY') config.fog_region = ENV['FOG_REGION'] if ENV.has_key?('FOG_REGION') - config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID'] if ENV.has_key?('AWS_ACCESS_KEY_ID') - config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY'] if ENV.has_key?('AWS_SECRET_ACCESS_KEY') + config.aws_access_key_id = ENV['ASSETS_AWS_ACCESS_KEY_ID'] if ENV.has_key?('ASSETS_AWS_ACCESS_KEY_ID') + config.aws_secret_access_key = ENV['ASSETS_AWS_SECRET_ACCESS_KEY'] if ENV.has_key?('ASSETS_AWS_SECRET_ACCESS_KEY') config.aws_reduced_redundancy = ENV['AWS_REDUCED_REDUNDANCY'] == true if ENV.has_key?('AWS_REDUCED_REDUNDANCY') config.rackspace_username = ENV['RACKSPACE_USERNAME'] if ENV.has_key?('RACKSPACE_USERNAME') diff --git a/config/routes/project.rb b/config/routes/project.rb index 239b5480321..c3ad53a387f 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -383,8 +383,8 @@ constraints(ProjectUrlConstrainer.new) do resources :runners, only: [:index, :edit, :update, :destroy, :show] do member do - get :resume - get :pause + post :resume + post :pause end collection do diff --git a/db/migrate/20171019141859_fix_dev_timezone_schema.rb b/db/migrate/20171019141859_fix_dev_timezone_schema.rb new file mode 100644 index 00000000000..fb7c17dd747 --- /dev/null +++ b/db/migrate/20171019141859_fix_dev_timezone_schema.rb @@ -0,0 +1,25 @@ +class FixDevTimezoneSchema < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # The this migrations tries to help solve unwanted changes to `schema.rb` + # while developing GitLab. Installations created before we started using + # `datetime_with_timezone` are likely to face this problem. Updating those + # columns to the new type should help fix this. + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + TIMEZONE_TABLES = %i(appearances ci_group_variables ci_pipeline_schedule_variables events gpg_keys gpg_signatures project_auto_devops) + + def up + return unless Rails.env.development? || Rails.env.test? + + TIMEZONE_TABLES.each do |table| + change_column table, :created_at, :datetime_with_timezone + change_column table, :updated_at, :datetime_with_timezone + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index aa5db5da4f0..88885f706b7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -657,8 +657,8 @@ ActiveRecord::Schema.define(version: 20171220191323) do t.datetime "created_at" t.datetime "updated_at" t.string "confirmation_token" - t.datetime "confirmed_at" - t.datetime "confirmation_sent_at" + t.datetime_with_timezone "confirmed_at" + t.datetime_with_timezone "confirmation_sent_at" end add_index "emails", ["confirmation_token"], name: "index_emails_on_confirmation_token", unique: true, using: :btree @@ -1770,8 +1770,8 @@ ActiveRecord::Schema.define(version: 20171220191323) do add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree create_table "user_custom_attributes", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false t.integer "user_id", null: false t.string "key", null: false t.string "value", null: false diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index 93c3642a1f1..65f59b72690 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -58,30 +58,32 @@ our AsciiDoc snippets, wikis and repos using delimited blocks: - **Markdown** - ```plantuml - Bob -> Alice : hello - Alice -> Bob : Go Away - ``` + <pre> + ```plantuml + Bob -> Alice : hello + Alice -> Bob : Go Away + ``` + </pre> - **AsciiDoc** - ``` + <pre> [plantuml, format="png", id="myDiagram", width="200px"] -- Bob->Alice : hello Alice -> Bob : Go Away -- - ``` + </pre> - **reStructuredText** - ``` + <pre> .. plantuml:: :caption: Caption with **bold** and *italic* Bob -> Alice: hello Alice -> Bob: Go Away - ``` + </pre> You can also use the `uml::` directive for compatibility with [sphinxcontrib-plantuml](https://pypi.python.org/pypi/sphinxcontrib-plantuml), but please note that we currently only support the `caption` option. diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 7dc234a9759..f77569e4886 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -173,6 +173,7 @@ where `PROJECT-1` is the issue ID of the JIRA project. - Only commits and merges into the project's default branch (usually **master**) will close an issue in Jira. You can change your projects default branch under [project settings](img/jira_project_settings.png). +- The JIRA issue will not be transitioned if it has a resolution. ### JIRA issue closing example @@ -222,6 +223,10 @@ JIRA issue references and update comments will not work if the GitLab issue trac Make sure the `Transition ID` you set within the JIRA settings matches the one your project needs to close a ticket. +Make sure that the JIRA issue is not already marked as resolved, in other words that +the JIRA issue resolution field is not set. (It should not be struck through in +JIRA lists.) + [services-templates]: services_templates.md [jira-repo-old-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md [jira]: https://www.atlassian.com/software/jira diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index e81e935e37d..442fc978284 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -39,3 +39,5 @@ do. | `/board_move ~column` | Move issue to column on the board | | `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue | | `/move path/to/project` | Moves issue to another project | +| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` | +| `/shrug` | Append the comment with `¯\_(ツ)_/¯` |
\ No newline at end of file diff --git a/lib/backup/files.rb b/lib/backup/files.rb index 30a91647b77..287d591e88d 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -18,7 +18,7 @@ module Backup FileUtils.rm_f(backup_tarball) if ENV['STRATEGY'] == 'copy' - cmd = %W(cp -a #{app_files_dir} #{Gitlab.config.backup.path}) + cmd = %W(rsync -a --exclude=lost+found #{app_files_dir} #{Gitlab.config.backup.path}) output, status = Gitlab::Popen.popen(cmd) unless status.zero? @@ -26,10 +26,10 @@ module Backup abort 'Backup failed' end - run_pipeline!([%W(tar -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) + run_pipeline!([%W(tar --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) FileUtils.rm_rf(@backup_files_dir) else - run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) + run_pipeline!([%W(tar --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) end end diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb index 582028493e9..6b53eb4533d 100644 --- a/lib/gitlab/encoding_helper.rb +++ b/lib/gitlab/encoding_helper.rb @@ -71,6 +71,16 @@ module Gitlab end end + def encode_binary(s) + return "" if s.nil? + + s.dup.force_encoding(Encoding::ASCII_8BIT) + end + + def binary_stringio(s) + StringIO.new(s || '').tap { |io| io.set_encoding(Encoding::ASCII_8BIT) } + end + private def clean(message) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index a09386d52a5..490bd753eda 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -126,7 +126,7 @@ module Gitlab end def exists? - Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled| + Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| if enabled gitaly_repository_client.exists? else @@ -188,7 +188,7 @@ module Gitlab end def local_branches(sort_by: nil) - gitaly_migrate(:local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| + gitaly_migrate(:local_branches) do |is_enabled| if is_enabled gitaly_ref_client.local_branches(sort_by: sort_by) else diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index b753ac46291..4507ea923b4 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -330,22 +330,6 @@ module Gitlab Google::Protobuf::Timestamp.new(seconds: t.to_i) end - def self.encode(s) - return "" if s.nil? - - s.dup.force_encoding(Encoding::ASCII_8BIT) - end - - def self.binary_stringio(s) - io = StringIO.new(s || '') - io.set_encoding(Encoding::ASCII_8BIT) - io - end - - def self.encode_repeated(a) - Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| self.encode(s) } ) - end - # The default timeout on all Gitaly calls def self.default_timeout return 0 if Sidekiq.server? diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index fb3e27770b4..8a29e8ec5b6 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -1,6 +1,8 @@ module Gitlab module GitalyClient class CommitService + include Gitlab::EncodingHelper + # The ID of empty tree. # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012 EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze @@ -13,7 +15,7 @@ module Gitlab def ls_files(revision) request = Gitaly::ListFilesRequest.new( repository: @gitaly_repo, - revision: GitalyClient.encode(revision) + revision: encode_binary(revision) ) response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request, timeout: GitalyClient.medium_timeout) @@ -73,7 +75,7 @@ module Gitlab request = Gitaly::TreeEntryRequest.new( repository: @gitaly_repo, revision: ref, - path: GitalyClient.encode(path), + path: encode_binary(path), limit: limit.to_i ) @@ -98,8 +100,8 @@ module Gitlab def tree_entries(repository, revision, path) request = Gitaly::GetTreeEntriesRequest.new( repository: @gitaly_repo, - revision: GitalyClient.encode(revision), - path: path.present? ? GitalyClient.encode(path) : '.' + revision: encode_binary(revision), + path: path.present? ? encode_binary(path) : '.' ) response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request, timeout: GitalyClient.medium_timeout) @@ -112,8 +114,8 @@ module Gitlab type: gitaly_tree_entry.type.downcase, mode: gitaly_tree_entry.mode.to_s(8), name: File.basename(gitaly_tree_entry.path), - path: GitalyClient.encode(gitaly_tree_entry.path), - flat_path: GitalyClient.encode(gitaly_tree_entry.flat_path), + path: encode_binary(gitaly_tree_entry.path), + flat_path: encode_binary(gitaly_tree_entry.flat_path), commit_id: gitaly_tree_entry.commit_oid ) end @@ -135,8 +137,8 @@ module Gitlab def last_commit_for_path(revision, path) request = Gitaly::LastCommitForPathRequest.new( repository: @gitaly_repo, - revision: GitalyClient.encode(revision), - path: GitalyClient.encode(path.to_s) + revision: encode_binary(revision), + path: encode_binary(path.to_s) ) gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request, timeout: GitalyClient.fast_timeout).commit @@ -202,8 +204,8 @@ module Gitlab def raw_blame(revision, path) request = Gitaly::RawBlameRequest.new( repository: @gitaly_repo, - revision: GitalyClient.encode(revision), - path: GitalyClient.encode(path) + revision: encode_binary(revision), + path: encode_binary(path) ) response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout) @@ -213,7 +215,7 @@ module Gitlab def find_commit(revision) request = Gitaly::FindCommitRequest.new( repository: @gitaly_repo, - revision: GitalyClient.encode(revision) + revision: encode_binary(revision) ) response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request, timeout: GitalyClient.medium_timeout) @@ -224,7 +226,7 @@ module Gitlab def patch(revision) request = Gitaly::CommitPatchRequest.new( repository: @gitaly_repo, - revision: GitalyClient.encode(revision) + revision: encode_binary(revision) ) response = GitalyClient.call(@repository.storage, :diff_service, :commit_patch, request, timeout: GitalyClient.medium_timeout) @@ -234,7 +236,7 @@ module Gitlab def commit_stats(revision) request = Gitaly::CommitStatsRequest.new( repository: @gitaly_repo, - revision: GitalyClient.encode(revision) + revision: encode_binary(revision) ) GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request, timeout: GitalyClient.medium_timeout) end @@ -250,9 +252,9 @@ module Gitlab ) request.after = GitalyClient.timestamp(options[:after]) if options[:after] request.before = GitalyClient.timestamp(options[:before]) if options[:before] - request.revision = GitalyClient.encode(options[:ref]) if options[:ref] + request.revision = encode_binary(options[:ref]) if options[:ref] - request.paths = GitalyClient.encode_repeated(Array(options[:path])) if options[:path].present? + request.paths = encode_repeated(Array(options[:path])) if options[:path].present? response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request, timeout: GitalyClient.medium_timeout) @@ -264,7 +266,7 @@ module Gitlab enum = Enumerator.new do |y| shas.each_slice(20) do |revs| - request.shas = GitalyClient.encode_repeated(revs) + request.shas = encode_repeated(revs) y.yield request @@ -303,7 +305,7 @@ module Gitlab repository: @gitaly_repo, left_commit_id: from_id, right_commit_id: to_id, - paths: options.fetch(:paths, []).compact.map { |path| GitalyClient.encode(path) } + paths: options.fetch(:paths, []).compact.map { |path| encode_binary(path) } } end @@ -314,6 +316,10 @@ module Gitlab end end end + + def encode_repeated(a) + Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| encode_binary(s) } ) + end end end end diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 400a4af363b..c7732764880 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -1,6 +1,8 @@ module Gitlab module GitalyClient class OperationService + include Gitlab::EncodingHelper + def initialize(repository) @gitaly_repo = repository.gitaly_repository @repository = repository @@ -9,7 +11,7 @@ module Gitlab def rm_tag(tag_name, user) request = Gitaly::UserDeleteTagRequest.new( repository: @gitaly_repo, - tag_name: GitalyClient.encode(tag_name), + tag_name: encode_binary(tag_name), user: Gitlab::Git::User.from_gitlab(user).to_gitaly ) @@ -24,9 +26,9 @@ module Gitlab request = Gitaly::UserCreateTagRequest.new( repository: @gitaly_repo, user: Gitlab::Git::User.from_gitlab(user).to_gitaly, - tag_name: GitalyClient.encode(tag_name), - target_revision: GitalyClient.encode(target), - message: GitalyClient.encode(message.to_s) + tag_name: encode_binary(tag_name), + target_revision: encode_binary(target), + message: encode_binary(message.to_s) ) response = GitalyClient.call(@repository.storage, :operation_service, :user_create_tag, request) @@ -44,9 +46,9 @@ module Gitlab def user_create_branch(branch_name, user, start_point) request = Gitaly::UserCreateBranchRequest.new( repository: @gitaly_repo, - branch_name: GitalyClient.encode(branch_name), + branch_name: encode_binary(branch_name), user: Gitlab::Git::User.from_gitlab(user).to_gitaly, - start_point: GitalyClient.encode(start_point) + start_point: encode_binary(start_point) ) response = GitalyClient.call(@repository.storage, :operation_service, :user_create_branch, request) @@ -64,7 +66,7 @@ module Gitlab def user_delete_branch(branch_name, user) request = Gitaly::UserDeleteBranchRequest.new( repository: @gitaly_repo, - branch_name: GitalyClient.encode(branch_name), + branch_name: encode_binary(branch_name), user: Gitlab::Git::User.from_gitlab(user).to_gitaly ) @@ -89,8 +91,8 @@ module Gitlab repository: @gitaly_repo, user: Gitlab::Git::User.from_gitlab(user).to_gitaly, commit_id: source_sha, - branch: GitalyClient.encode(target_branch), - message: GitalyClient.encode(message) + branch: encode_binary(target_branch), + message: encode_binary(message) ) ) @@ -111,7 +113,7 @@ module Gitlab repository: @gitaly_repo, user: Gitlab::Git::User.from_gitlab(user).to_gitaly, commit_id: source_sha, - branch: GitalyClient.encode(target_branch) + branch: encode_binary(target_branch) ) branch_update = GitalyClient.call( @@ -152,9 +154,9 @@ module Gitlab repository: @gitaly_repo, user: Gitlab::Git::User.from_gitlab(user).to_gitaly, commit: commit.to_gitaly_commit, - branch_name: GitalyClient.encode(branch_name), - message: GitalyClient.encode(message), - start_branch_name: GitalyClient.encode(start_branch_name.to_s), + branch_name: encode_binary(branch_name), + message: encode_binary(message), + start_branch_name: encode_binary(start_branch_name.to_s), start_repository: start_repository.gitaly_repository ) diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index 066e4e183c0..5bce1009878 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -72,7 +72,7 @@ module Gitlab end def ref_exists?(ref_name) - request = Gitaly::RefExistsRequest.new(repository: @gitaly_repo, ref: GitalyClient.encode(ref_name)) + request = Gitaly::RefExistsRequest.new(repository: @gitaly_repo, ref: encode_binary(ref_name)) response = GitalyClient.call(@storage, :ref_service, :ref_exists, request) response.value rescue GRPC::InvalidArgument => e @@ -82,7 +82,7 @@ module Gitlab def find_branch(branch_name) request = Gitaly::FindBranchRequest.new( repository: @gitaly_repo, - name: GitalyClient.encode(branch_name) + name: encode_binary(branch_name) ) response = GitalyClient.call(@repository.storage, :ref_service, :find_branch, request) @@ -96,8 +96,8 @@ module Gitlab def create_branch(ref, start_point) request = Gitaly::CreateBranchRequest.new( repository: @gitaly_repo, - name: GitalyClient.encode(ref), - start_point: GitalyClient.encode(start_point) + name: encode_binary(ref), + start_point: encode_binary(start_point) ) response = GitalyClient.call(@repository.storage, :ref_service, :create_branch, request) @@ -121,7 +121,7 @@ module Gitlab def delete_branch(branch_name) request = Gitaly::DeleteBranchRequest.new( repository: @gitaly_repo, - name: GitalyClient.encode(branch_name) + name: encode_binary(branch_name) ) GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request) diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index c1f95396878..2115f388d5b 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -1,6 +1,8 @@ module Gitlab module GitalyClient class RepositoryService + include Gitlab::EncodingHelper + def initialize(repository) @repository = repository @gitaly_repo = repository.gitaly_repository @@ -72,7 +74,7 @@ module Gitlab def find_merge_base(*revisions) request = Gitaly::FindMergeBaseRequest.new( repository: @gitaly_repo, - revisions: revisions.map { |r| GitalyClient.encode(r) } + revisions: revisions.map { |r| encode_binary(r) } ) response = GitalyClient.call(@storage, :repository_service, :find_merge_base, request) diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb index 337d225d081..5c5b170a3e0 100644 --- a/lib/gitlab/gitaly_client/wiki_service.rb +++ b/lib/gitlab/gitaly_client/wiki_service.rb @@ -3,6 +3,8 @@ require 'stringio' module Gitlab module GitalyClient class WikiService + include Gitlab::EncodingHelper + MAX_MSG_SIZE = 128.kilobytes.freeze def initialize(repository) @@ -13,12 +15,12 @@ module Gitlab def write_page(name, format, content, commit_details) request = Gitaly::WikiWritePageRequest.new( repository: @gitaly_repo, - name: GitalyClient.encode(name), + name: encode_binary(name), format: format.to_s, commit_details: gitaly_commit_details(commit_details) ) - strio = GitalyClient.binary_stringio(content) + strio = binary_stringio(content) enum = Enumerator.new do |y| until strio.eof? @@ -39,13 +41,13 @@ module Gitlab def update_page(page_path, title, format, content, commit_details) request = Gitaly::WikiUpdatePageRequest.new( repository: @gitaly_repo, - page_path: GitalyClient.encode(page_path), - title: GitalyClient.encode(title), + page_path: encode_binary(page_path), + title: encode_binary(title), format: format.to_s, commit_details: gitaly_commit_details(commit_details) ) - strio = GitalyClient.binary_stringio(content) + strio = binary_stringio(content) enum = Enumerator.new do |y| until strio.eof? @@ -63,7 +65,7 @@ module Gitlab def delete_page(page_path, commit_details) request = Gitaly::WikiDeletePageRequest.new( repository: @gitaly_repo, - page_path: GitalyClient.encode(page_path), + page_path: encode_binary(page_path), commit_details: gitaly_commit_details(commit_details) ) @@ -73,9 +75,9 @@ module Gitlab def find_page(title:, version: nil, dir: nil) request = Gitaly::WikiFindPageRequest.new( repository: @gitaly_repo, - title: GitalyClient.encode(title), - revision: GitalyClient.encode(version), - directory: GitalyClient.encode(dir) + title: encode_binary(title), + revision: encode_binary(version), + directory: encode_binary(dir) ) response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request) @@ -102,8 +104,8 @@ module Gitlab def find_file(name, revision) request = Gitaly::WikiFindFileRequest.new( repository: @gitaly_repo, - name: GitalyClient.encode(name), - revision: GitalyClient.encode(revision) + name: encode_binary(name), + revision: encode_binary(revision) ) response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_file, request) @@ -158,9 +160,9 @@ module Gitlab def gitaly_commit_details(commit_details) Gitaly::WikiCommitDetails.new( - name: GitalyClient.encode(commit_details.name), - email: GitalyClient.encode(commit_details.email), - message: GitalyClient.encode(commit_details.message) + name: encode_binary(commit_details.name), + email: encode_binary(commit_details.email), + message: encode_binary(commit_details.message) ) end end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 11472ce6cce..6ced06a863d 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -57,11 +57,17 @@ module Gitlab } end - def highest_allowed_level + def allowed_levels restricted_levels = current_application_settings.restricted_visibility_levels - allowed_levels = self.values - restricted_levels - allowed_levels.max || PRIVATE + self.values - restricted_levels + end + + def closest_allowed_level(target_level) + highest_allowed_level = allowed_levels.select { |level| level <= target_level }.max + + # If all levels are restricted, fall back to PRIVATE + highest_allowed_level || PRIVATE end def allowed_for?(user, level) diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 18ae45aa340..ef40dddfd3a 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -176,6 +176,7 @@ describe 'Dropdown hint', :js do it 'reuses existing author text' do filtered_search.send_keys('author:') filtered_search.send_keys(:backspace) + filtered_search.send_keys(:backspace) click_hint('author') expect_tokens([{ name: 'author' }]) @@ -185,6 +186,7 @@ describe 'Dropdown hint', :js do it 'reuses existing assignee text' do filtered_search.send_keys('assignee:') filtered_search.send_keys(:backspace) + filtered_search.send_keys(:backspace) click_hint('assignee') expect_tokens([{ name: 'assignee' }]) @@ -194,6 +196,7 @@ describe 'Dropdown hint', :js do it 'reuses existing milestone text' do filtered_search.send_keys('milestone:') filtered_search.send_keys(:backspace) + filtered_search.send_keys(:backspace) click_hint('milestone') expect_tokens([{ name: 'milestone' }]) @@ -203,6 +206,7 @@ describe 'Dropdown hint', :js do it 'reuses existing label text' do filtered_search.send_keys('label:') filtered_search.send_keys(:backspace) + filtered_search.send_keys(:backspace) click_hint('label') expect_tokens([{ name: 'label' }]) @@ -212,6 +216,7 @@ describe 'Dropdown hint', :js do it 'reuses existing emoji text' do filtered_search.send_keys('my-reaction:') filtered_search.send_keys(:backspace) + filtered_search.send_keys(:backspace) click_hint('my-reaction') expect_tokens([{ name: 'my-reaction' }]) diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb index 7d5ba3a7328..b04a5422fed 100644 --- a/spec/features/profiles/keys_spec.rb +++ b/spec/features/profiles/keys_spec.rb @@ -27,6 +27,7 @@ feature 'Profile > SSH Keys' do expect(page).to have_content("Title: #{attrs[:title]}") expect(page).to have_content(attrs[:key]) + expect(find('.breadcrumbs-sub-title')).to have_link(attrs[:title]) end context 'when only DSA and ECDSA keys are allowed' do diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb index d1edeef8da4..7d204f89fba 100644 --- a/spec/features/profiles/oauth_applications_spec.rb +++ b/spec/features/profiles/oauth_applications_spec.rb @@ -2,12 +2,20 @@ require 'spec_helper' describe 'Profile > Applications' do let(:user) { create(:user) } + let(:application) { create(:oauth_application, owner: user) } before do sign_in(user) end describe 'User manages applications', :js do + it 'views an application' do + visit oauth_application_path(application) + + expect(page).to have_content("Application: #{application.name}") + expect(find('.breadcrumbs-sub-title')).to have_link(application.name) + end + it 'deletes an application' do create(:oauth_application, owner: user) visit oauth_applications_path diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index c7f0e342809..aec9de6c7ca 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -33,6 +33,26 @@ feature 'Runners' do expect(page).to have_content(specific_runner.platform) end + scenario 'user can pause and resume the specific runner' do + visit runners_path(project) + + within '.activated-specific-runners' do + expect(page).to have_content('Pause') + end + + click_on 'Pause' + + within '.activated-specific-runners' do + expect(page).to have_content('Resume') + end + + click_on 'Resume' + + within '.activated-specific-runners' do + expect(page).to have_content('Pause') + end + end + scenario 'user removes an activated specific runner if this is last project for that runners' do visit runners_path(project) diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 5111632d681..b8890e4cda1 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -252,6 +252,7 @@ describe('Filtered Search Manager', () => { it('removes last token', () => { spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); dispatchBackspaceEvent(input, 'keyup'); + dispatchBackspaceEvent(input, 'keyup'); expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); }); @@ -259,6 +260,7 @@ describe('Filtered Search Manager', () => { it('sets the input', () => { spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); dispatchDeleteEvent(input, 'keyup'); + dispatchDeleteEvent(input, 'keyup'); expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); expect(input.value).toEqual('~bug'); @@ -276,6 +278,18 @@ describe('Filtered Search Manager', () => { expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); expect(input.value).toEqual('text'); }); + + it('does not remove previous token on single backspace press', () => { + spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + + input.value = 't'; + dispatchDeleteEvent(input, 'keyup'); + + expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); + expect(input.value).toEqual('t'); + }); }); describe('removeToken', () => { diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index f6e5c55240f..87ec2698fc1 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -145,4 +145,18 @@ describe Gitlab::EncodingHelper do end end end + + describe 'encode_binary' do + [ + [nil, ""], + ["", ""], + [" ", " "], + %w(a1 a1), + ["编码", "\xE7\xBC\x96\xE7\xA0\x81".b] + ].each do |input, result| + it "encodes #{input.inspect} to #{result.inspect}" do + expect(ext_class.encode_binary(input)).to eq(result) + end + end + end end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index a871ed0df0e..309b7338ef0 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -38,20 +38,6 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do end end - describe 'encode' do - [ - [nil, ""], - ["", ""], - [" ", " "], - %w(a1 a1), - ["编码", "\xE7\xBC\x96\xE7\xA0\x81".b] - ].each do |input, result| - it "encodes #{input.inspect} to #{result.inspect}" do - expect(described_class.encode(input)).to eq result - end - end - end - describe 'allow_n_plus_1_calls' do context 'when RequestStore is enabled', :request_store do it 'returns the result of the allow_n_plus_1_calls block' do diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 48a67773de9..d85dac630b4 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -49,4 +49,31 @@ describe Gitlab::VisibilityLevel do .to eq([Gitlab::VisibilityLevel::PUBLIC]) end end + + describe '.allowed_levels' do + it 'only includes the levels that arent restricted' do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) + + expect(described_class.allowed_levels) + .to contain_exactly(described_class::PRIVATE, described_class::PUBLIC) + end + end + + describe '.closest_allowed_level' do + it 'picks INTERNAL instead of PUBLIC if public is restricted' do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + + expect(described_class.closest_allowed_level(described_class::PUBLIC)) + .to eq(described_class::INTERNAL) + end + + it 'picks PRIVATE if nothing is available' do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PRIVATE]) + + expect(described_class.closest_allowed_level(described_class::PUBLIC)) + .to eq(described_class::PRIVATE) + end + end end diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb index fa02434b0fd..50b19000799 100644 --- a/spec/models/diff_discussion_spec.rb +++ b/spec/models/diff_discussion_spec.rb @@ -47,8 +47,20 @@ describe DiffDiscussion do diff_note.save! end - it 'returns the diff ID for the version to show' do - expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff1.id) + context 'when commit_id is not present' do + it 'returns the diff ID for the version to show' do + expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff1.id) + end + end + + context 'when commit_id is present' do + before do + diff_note.update_attribute(:commit_id, 'commit_123') + end + + it 'includes the commit_id in the result' do + expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff1.id, commit_id: 'commit_123') + end end end @@ -70,8 +82,20 @@ describe DiffDiscussion do diff_note.save! end - it 'returns the diff ID and start sha of the versions to compare' do - expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha) + context 'when commit_id is not present' do + it 'returns the diff ID and start sha of the versions to compare' do + expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha) + end + end + + context 'when commit_id is present' do + before do + diff_note.update_attribute(:commit_id, 'commit_123') + end + + it 'includes the commit_id in the result' do + expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha, commit_id: 'commit_123') + end end end @@ -83,8 +107,20 @@ describe DiffDiscussion do diff_note.save! end - it 'returns nil' do - expect(subject.merge_request_version_params).to be_nil + context 'when commit_id is not present' do + it 'returns empty hash' do + expect(subject.merge_request_version_params).to eq(nil) + end + end + + context 'when commit_id is present' do + before do + diff_note.update_attribute(:commit_id, 'commit_123') + end + + it 'returns the commit_id' do + expect(subject.merge_request_version_params).to eq(commit_id: 'commit_123') + end end end end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 4057caca2ac..409d5de8d43 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -139,10 +139,10 @@ describe Projects::ForkService do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) end - it "creates fork with highest allowed level" do + it "creates fork with lowest level" do forked_project = fork_project(@from_project, @to_user) - expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) end end @@ -209,6 +209,19 @@ describe Projects::ForkService do expect(to_project.errors[:path]).to eq(['has already been taken']) end end + + context 'when the namespace has a lower visibility level than the project' do + it 'creates the project with the lower visibility level' do + public_project = create(:project, :public) + private_group = create(:group, :private) + group_owner = create(:user) + private_group.add_owner(group_owner) + + forked_project = fork_project(public_project, group_owner, namespace: private_group) + + expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 9025589ae0b..4e640a82dfc 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe SystemNoteService do include Gitlab::Routing + include RepoHelpers set(:group) { create(:group) } set(:project) { create(:project, :repository, group: group) } @@ -1070,17 +1071,32 @@ describe SystemNoteService do let(:action) { 'outdated' } end - it 'creates a new note in the discussion' do - # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. - expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1) + context 'when the change_position is valid for the discussion' do + it 'creates a new note in the discussion' do + # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. + expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1) + end + + it 'links to the diff in the system note' do + expect(subject.note).to include('version 1') + + diff_id = merge_request.merge_request_diff.id + line_code = change_position.line_code(project.repository) + expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code)) + end end - it 'links to the diff in the system note' do - expect(subject.note).to include('version 1') + context 'when the change_position is invalid for the discussion' do + let(:change_position) { project.commit(sample_commit.id) } - diff_id = merge_request.merge_request_diff.id - line_code = change_position.line_code(project.repository) - expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code)) + it 'creates a new note in the discussion' do + # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. + expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1) + end + + it 'does not create a link' do + expect(subject.note).to eq('changed this line in version 1 of the diff') + end end end |