diff options
72 files changed, 900 insertions, 266 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57d94dad672..1fd29fef4f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,6 +94,10 @@ look for [issues with the label `Accepting Merge Requests` and weight < 5][accep These issues will be of reasonable size and challenge, for anyone to start contributing to GitLab. +## Workflow labels + +Labelling issues is described in the [GitLab Inc engineering workflow]. + ## Implement design & UI elements Please see the [UX Guide for GitLab]. @@ -535,6 +539,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" [UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/ [license-finder-doc]: doc/development/licensing.md +[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues [^1]: Specs other than JavaScript specs are considered backend code. Haml changes are considered backend code if they include Ruby code other than just diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 54836efdf29..8a077f0081a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -45,12 +45,12 @@ function buildCategoryMap() { }); } -function renderCategory(name, emojiList) { +function renderCategory(name, emojiList, opts = {}) { return ` <h5 class="emoji-menu-title"> ${name} </h5> - <ul class="clearfix emoji-menu-list"> + <ul class="clearfix emoji-menu-list ${opts.menuListClass}"> ${emojiList.map(emojiName => ` <li class="emoji-menu-list-item"> <button class="emoji-menu-btn text-center js-emoji-btn" type="button"> @@ -140,9 +140,6 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { const $createdMenu = $('.emoji-menu'); $addBtn.removeClass('is-loading'); this.positionMenu($createdMenu, $addBtn); - if (!this.frequentEmojiBlockRendered) { - this.renderFrequentlyUsedBlock(); - } return setTimeout(() => { $createdMenu.addClass('is-visible'); $('#emoji_search').focus(); @@ -165,11 +162,21 @@ AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) { const emojisInCategory = categoryMap[categoryNameKey]; const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); + // Render the frequently used + const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); + let frequentlyUsedCatgegory = ''; + if (frequentlyUsedEmojis.length > 0) { + frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, { + menuListClass: 'frequent-emojis', + }); + } + const emojiMenuMarkup = ` <div class="emoji-menu"> <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" /> <div class="emoji-menu-content"> + ${frequentlyUsedCatgegory} ${firstCategory} </div> </div> @@ -457,19 +464,6 @@ AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmoj return _.compact(_.uniq(frequentlyUsedEmojis)); }; -AwardsHandler.prototype.renderFrequentlyUsedBlock = function renderFrequentlyUsedBlock() { - if (Cookies.get('frequently_used_emojis')) { - const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); - const ul = $('<ul class="clearfix emoji-menu-list frequent-emojis">'); - for (let i = 0, len = frequentlyUsedEmojis.length; i < len; i += 1) { - const emoji = frequentlyUsedEmojis[i]; - $(`.emoji-menu-content [data-name="${emoji}"]`).closest('li').clone().appendTo(ul); - } - $('.emoji-menu-content').prepend(ul).prepend($('<h5>').text('Frequently used')); - } - this.frequentEmojiBlockRendered = true; -}; - AwardsHandler.prototype.setupSearch = function setupSearch() { this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => { const term = $(e.target).val().trim(); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 017980271b1..7b9b9123c31 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -39,6 +39,7 @@ import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; import GroupsList from './groups_list'; import ProjectsList from './projects_list'; +import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; const ShortcutsBlob = require('./shortcuts_blob'); const UserCallout = require('./user_callout'); @@ -181,7 +182,7 @@ const UserCallout = require('./user_callout'); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:commit:pipelines': - new gl.MiniPipelineGraph({ + new MiniPipelineGraph({ container: '.js-pipeline-table', }).bindEvents(); break; diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index 5f1bd474a0c..66cc270ab4d 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -3,7 +3,8 @@ /* global notifyPermissions */ /* global merge_request_widget */ -require('./smart_interval'); +import './smart_interval'; +import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; @@ -285,7 +286,7 @@ require('./smart_interval'); }; MergeRequestWidget.prototype.initMiniPipelineGraph = function() { - new gl.MiniPipelineGraph({ + new MiniPipelineGraph({ container: '.js-pipeline-inline-mr-widget-graph:visible', }).bindEvents(); }; diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index 2145e531331..9c58c465001 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -15,81 +15,96 @@ * <div class="js-builds-dropdown-container dropdown-menu"></div> * </div> */ -(() => { - class MiniPipelineGraph { - constructor(opts = {}) { - this.container = opts.container || ''; - this.dropdownListSelector = '.js-builds-dropdown-container'; - this.getBuildsList = this.getBuildsList.bind(this); - } - /** - * Adds the event listener when the dropdown is opened. - * All dropdown events are fired at the .dropdown-menu's parent element. - */ - bindEvents() { - $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); - } +export default class MiniPipelineGraph { + constructor(opts = {}) { + this.container = opts.container || ''; + this.dropdownListSelector = '.js-builds-dropdown-container'; + this.getBuildsList = this.getBuildsList.bind(this); + } + + /** + * Adds the event listener when the dropdown is opened. + * All dropdown events are fired at the .dropdown-menu's parent element. + */ + bindEvents() { + $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); + } - /** - * For the clicked stage, renders the given data in the dropdown list. - * - * @param {HTMLElement} stageContainer - * @param {Object} data - */ - renderBuildsList(stageContainer, data) { - const dropdownContainer = stageContainer.parentElement.querySelector( - `${this.dropdownListSelector} .js-builds-dropdown-list`, - ); + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(document).on( + 'click', + `${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`, + (e) => { + e.stopPropagation(); + }, + ); + } - dropdownContainer.innerHTML = data; - } + /** + * For the clicked stage, renders the given data in the dropdown list. + * + * @param {HTMLElement} stageContainer + * @param {Object} data + */ + renderBuildsList(stageContainer, data) { + const dropdownContainer = stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-list`, + ); - /** - * For the clicked stage, gets the list of builds. - * - * All dropdown events have a relatedTarget property, - * whose value is the toggling anchor element. - * - * @param {Object} e bootstrap dropdown event - * @return {Promise} - */ - getBuildsList(e) { - const button = e.relatedTarget; - const endpoint = button.dataset.stageEndpoint; + dropdownContainer.innerHTML = data; + } - return $.ajax({ - dataType: 'json', - type: 'GET', - url: endpoint, - beforeSend: () => { - this.renderBuildsList(button, ''); - this.toggleLoading(button); - }, - success: (data) => { - this.toggleLoading(button); - this.renderBuildsList(button, data.html); - }, - error: () => { - this.toggleLoading(button); - new Flash('An error occurred while fetching the builds.', 'alert'); - }, - }); - } + /** + * For the clicked stage, gets the list of builds. + * + * All dropdown events have a relatedTarget property, + * whose value is the toggling anchor element. + * + * @param {Object} e bootstrap dropdown event + * @return {Promise} + */ + getBuildsList(e) { + const button = e.relatedTarget; + const endpoint = button.dataset.stageEndpoint; - /** - * Toggles the visibility of the loading icon. - * - * @param {HTMLElement} stageContainer - * @return {type} - */ - toggleLoading(stageContainer) { - stageContainer.parentElement.querySelector( - `${this.dropdownListSelector} .js-builds-dropdown-loading`, - ).classList.toggle('hidden'); - } + return $.ajax({ + dataType: 'json', + type: 'GET', + url: endpoint, + beforeSend: () => { + this.renderBuildsList(button, ''); + this.toggleLoading(button); + }, + success: (data) => { + this.toggleLoading(button); + this.renderBuildsList(button, data.html); + this.stopDropdownClickPropagation(); + }, + error: () => { + this.toggleLoading(button); + new Flash('An error occurred while fetching the builds.', 'alert'); + }, + }); } - window.gl = window.gl || {}; - window.gl.MiniPipelineGraph = MiniPipelineGraph; -})(); + /** + * Toggles the visibility of the loading icon. + * + * @param {HTMLElement} stageContainer + * @return {type} + */ + toggleLoading(stageContainer) { + stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-loading`, + ).classList.toggle('hidden'); + } +} diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index eeab69da941..47cc34e7a20 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,12 +1,14 @@ /* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */ /* global Flash */ /* global Autosave */ +/* global Cookies */ /* global ResolveService */ /* global mrRefreshWidgetUrl */ require('./autosave'); window.autosize = require('vendor/autosize'); window.Dropzone = require('dropzone'); +window.Cookies = require('js-cookie'); require('./dropzone_input'); require('./gfm_auto_complete'); require('vendor/jquery.caret'); // required by jquery.atwho @@ -42,7 +44,6 @@ require('./task_list'); this.notes_url = notes_url; this.note_ids = note_ids; this.last_fetched_at = last_fetched_at; - this.view = view; this.noteable_url = document.URL; this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge")); this.basePollingInterval = 15000; @@ -57,6 +58,7 @@ require('./task_list'); selector: '.notes' }); this.collapseLongCommitList(); + this.setViewType(view); // We are in the Merge Requests page so we need another edit form for Changes tab if (gl.utils.getPagePath(1) === 'merge_requests') { @@ -65,6 +67,10 @@ require('./task_list'); } } + Notes.prototype.setViewType = function(view) { + this.view = Cookies.get('diff_view') || view; + }; + Notes.prototype.addBinding = function() { // add note to UI after creation $(document).on("ajax:success", ".js-main-target-form", this.addNote); @@ -302,7 +308,7 @@ require('./task_list'); }; Notes.prototype.isParallelView = function() { - return this.view === 'parallel'; + return Cookies.get('diff_view') === 'parallel'; }; /* diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index fe8b37d2c6e..186bb9ac616 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -43,7 +43,7 @@ white-space: nowrap; &[disabled] { - background-color: $input-bg-disabled; + opacity: .65; cursor: not-allowed; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index d3496e19dde..7c3172421c1 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -24,10 +24,6 @@ color: inherit; } - .btn-success.dropdown-toggle:disabled { - background-color: $gl-success; - } - .accept-merge-request { &.ci-pending, &.ci-running { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index dc79de19d48..c8ad88e9351 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -384,7 +384,7 @@ ul.notes { top: 0; .note-action-button { - margin-left: 10px; + margin-left: 8px; } } @@ -400,8 +400,7 @@ ul.notes { } .note-action-button { - display: inline-block; - margin-left: 0; + display: inline; line-height: 20px; @media (min-width: $screen-sm-min) { @@ -540,7 +539,6 @@ ul.notes { } .line-resolve-btn { - display: inline-block; position: relative; top: 2px; padding: 0; @@ -563,8 +561,9 @@ ul.notes { } svg { - position: relative; fill: $gray-darkest; + height: 15px; + width: 15px; } } diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index d7a45bacd35..b79ca034c5b 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -18,8 +18,7 @@ class AutocompleteController < ApplicationController if params[:search].blank? # Include current user if available to filter by "Me" if params[:current_user].present? && current_user - @users = @users.where.not(id: current_user.id) - @users = [current_user, *@users] + @users = [current_user, *@users].uniq end if params[:author_id].present? diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 17cb1d5be24..f9d798d0455 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -13,7 +13,8 @@ class Projects::ServicesController < Projects::ApplicationController end def update - if @service.update_attributes(service_params[:service]) + @service.assign_attributes(service_params[:service]) + if @service.save(context: :manual_change) redirect_to( edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), notice: 'Successfully updated.' diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index c2b399041c6..aad83731b87 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -1,4 +1,6 @@ module IssuablesHelper + include GitlabRoutingHelper + def sidebar_gutter_toggle_icon sidebar_gutter_collapsed? ? icon('angle-double-left', { 'aria-hidden': 'true' }) : icon('angle-double-right', { 'aria-hidden': 'true' }) end @@ -95,8 +97,23 @@ module IssuablesHelper h(milestone_title.presence || default_label) end + def to_url_reference(issuable) + case issuable + when Issue + link_to issuable.to_reference, issue_url(issuable) + when MergeRequest + link_to issuable.to_reference, merge_request_url(issuable) + else + issuable.to_reference + end + end + def issuable_meta(issuable, project, text) - output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier" + output = content_tag(:strong, class: "identifier") do + concat("#{text} ") + concat(to_url_reference(issuable)) + end + output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe output << content_tag(:strong) do author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 67206415f7b..8a5a9aa4adb 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -144,7 +144,7 @@ module Ci status_sql = statuses.latest.where('stage=sg.stage').status_sql - warnings_sql = statuses.latest.select('COUNT(*) > 0') + warnings_sql = statuses.latest.select('COUNT(*)') .where('stage=sg.stage').failed_but_allowed.to_sql stages_with_statuses = CommitStatus.from(stages_query, :sg) diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index ca74c91b062..e7d6b17d445 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -46,10 +46,10 @@ module Ci end def has_warnings? - if @warnings.nil? - statuses.latest.failed_but_allowed.any? + if @warnings.is_a?(Integer) + @warnings > 0 else - @warnings + statuses.latest.failed_but_allowed.any? end end end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 9e65fdbf9d6..50435b67eda 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,4 +1,6 @@ class IssueTrackerService < Service + validate :one_issue_tracker, if: :activated?, on: :manual_change + default_value_for :category, 'issue_tracker' # Pattern used to extract links from comments @@ -92,4 +94,13 @@ class IssueTrackerService < Service def issues_tracker Gitlab.config.issues_tracker[to_param] end + + def one_issue_tracker + return if template? + return if project.blank? + + if project.services.external_issue_trackers.where.not(id: id).any? + errors.add(:base, 'Another issue tracker is already in use. Only one issue tracker service can be active at a time') + end + end end diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index bdea1064096..06fb531b546 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: url_for(params), rel: "self", type: "application/atom+xml" xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" xml.id issues_dashboard_url - xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any? + xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? end diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 30e63d991bb..a2f6a7ab1cb 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -4,7 +4,7 @@ .devise-errors = devise_error_messages! .form-group - = f.label :name + = f.label :name, 'Full name' = f.text_field :name, class: "form-control top", required: true, title: "This field is required." .username.form-group = f.label :username diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder index 7890e717aa7..43a52cf3002 100644 --- a/app/views/events/_event.atom.builder +++ b/app/views/events/_event.atom.builder @@ -4,7 +4,7 @@ xml.entry do xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" xml.link href: event_feed_url(event) xml.title truncate(event_feed_title(event), length: 80) - xml.updated event.created_at.xmlschema + xml.updated event.updated_at.xmlschema xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) xml.author do diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index 0cc6466d34e..469768d83f2 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: url_for(params), rel: "self", type: "application/atom+xml" xml.link href: issues_group_url, rel: "alternate", type: "text/html" xml.id issues_group_url - xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any? + xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? end diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder index 96831874144..fcd30c8c765 100644 --- a/app/views/issues/_issue.atom.builder +++ b/app/views/issues/_issue.atom.builder @@ -2,7 +2,7 @@ xml.entry do xml.id namespace_project_issue_url(issue.project.namespace, issue.project, issue) xml.link href: namespace_project_issue_url(issue.project.namespace, issue.project, issue) xml.title truncate(issue.title, length: 80) - xml.updated issue.created_at.xmlschema + xml.updated issue.updated_at.xmlschema xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) xml.author do diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 0cbe9b3275a..4cfbd9add00 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -3,7 +3,7 @@ %h4.prepend-top-0 Deploy Keys %p - Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. + Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. .col-lg-9 %h5.prepend-top-0 Create a new deploy key for this project diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index a0df0db77c5..4feec09bb5d 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: url_for(params), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_issues_url(@project.namespace, @project) - xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any? + xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? end diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index a7618370a5d..5552086bc50 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -43,18 +43,17 @@ "inline-template" => true, "ref" => "note_#{note.id}" } - .note-action-button + %button.note-action-button.line-resolve-btn{ type: "button", + class: ("is-disabled" unless can_resolve), + ":class" => "{ 'is-active': isResolved }", + ":aria-label" => "buttonText", + "@click" => "resolve", + ":title" => "buttonText", + "v-show" => "!loading", + ":ref" => "'button'" } = icon("spin spinner", "v-show" => "loading") - %button.line-resolve-btn{ type: "button", - class: ("is-disabled" unless can_resolve), - ":class" => "{ 'is-active': isResolved }", - ":aria-label" => "buttonText", - "@click" => "resolve", - ":title" => "buttonText", - "v-show" => "!loading", - ":ref" => "'button'" } - = render "shared/icons/icon_status_success.svg" + = render "shared/icons/icon_status_success.svg" - if current_user - if note.emoji_awardable? diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index afd37e76c79..f8123846596 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -14,18 +14,18 @@ .scroll-container %ul.tokens-container.list-unstyled %li.input-token - %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) } + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: 'filtered-search', 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } } = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-action' => 'submit' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { action: 'submit' } } %button.btn.btn-link = icon('search') %span Keep typing and press Enter - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link -# Encapsulate static class name `{{icon}}` inside #{} to bypass @@ -36,50 +36,50 @@ %span.js-filter-tag.dropdown-light-content {{tag}} #js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } } - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } .dropdown-user-details %span {{name}} %span.dropdown-light-content @{{username}} #js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Assignee %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } .dropdown-user-details %span {{name}} %span.dropdown-light-content @{{username}} #js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Milestone - %li.filter-dropdown-item{ 'data-value' => 'upcoming' } + %li.filter-dropdown-item{ data: { value: 'upcoming' } } %button.btn.btn-link Upcoming %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.js-data-value {{title}} #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Label %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ style: 'background: {{color}}' } diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index 0e20df506a3..13207a8bc71 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -10,7 +10,7 @@ class AuthorizedProjectsWorker end def self.bulk_perform_async(args_list) - Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) + Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list) end def perform(user_id) diff --git a/changelogs/unreleased/24137-issuable-permalink.yml b/changelogs/unreleased/24137-issuable-permalink.yml new file mode 100644 index 00000000000..bcc6c6957a1 --- /dev/null +++ b/changelogs/unreleased/24137-issuable-permalink.yml @@ -0,0 +1,4 @@ +--- +title: Link issuable reference to itself in meta-header +merge_request: 9641 +author: mhasbini diff --git a/changelogs/unreleased/24166-close-builds-dropdown.yml b/changelogs/unreleased/24166-close-builds-dropdown.yml new file mode 100644 index 00000000000..c57ffed6b45 --- /dev/null +++ b/changelogs/unreleased/24166-close-builds-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Prevent builds dropdown to close when the user clicks in a build +merge_request: +author: diff --git a/changelogs/unreleased/29046-fix-github-importer-open-prs.yml b/changelogs/unreleased/29046-fix-github-importer-open-prs.yml new file mode 100644 index 00000000000..d279c269f94 --- /dev/null +++ b/changelogs/unreleased/29046-fix-github-importer-open-prs.yml @@ -0,0 +1,4 @@ +--- +title: Fix GitHub Import deleting branches for open PRs from a fork +merge_request: 9758 +author: diff --git a/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml b/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml new file mode 100644 index 00000000000..0de7754badc --- /dev/null +++ b/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml @@ -0,0 +1,4 @@ +--- +title: Make authorized projects worker use a specific queue instead of the default one +merge_request: 9813 +author: diff --git a/changelogs/unreleased/29189-discussion-button.yml b/changelogs/unreleased/29189-discussion-button.yml new file mode 100644 index 00000000000..eea96362117 --- /dev/null +++ b/changelogs/unreleased/29189-discussion-button.yml @@ -0,0 +1,4 @@ +--- +title: Fix alignment of resolve button +merge_request: +author: diff --git a/changelogs/unreleased/29209-sign-up-form-name.yml b/changelogs/unreleased/29209-sign-up-form-name.yml new file mode 100644 index 00000000000..e8e3a71f875 --- /dev/null +++ b/changelogs/unreleased/29209-sign-up-form-name.yml @@ -0,0 +1,4 @@ +--- +title: Change label for name on sign up form +merge_request: +author: diff --git a/changelogs/unreleased/29263-merge-button-color.yml b/changelogs/unreleased/29263-merge-button-color.yml new file mode 100644 index 00000000000..2d0625483a4 --- /dev/null +++ b/changelogs/unreleased/29263-merge-button-color.yml @@ -0,0 +1,4 @@ +--- +title: ensure MR widget dropdown is same color as button +merge_request: +author: diff --git a/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml b/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml new file mode 100644 index 00000000000..dabf9968c5b --- /dev/null +++ b/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml @@ -0,0 +1,4 @@ +--- +title: Add custom attributes in factories +merge_request: 9892 +author: George Andrinopoulos diff --git a/changelogs/unreleased/adam-prevent-two-issue-trackers.yml b/changelogs/unreleased/adam-prevent-two-issue-trackers.yml new file mode 100644 index 00000000000..307b7ec7359 --- /dev/null +++ b/changelogs/unreleased/adam-prevent-two-issue-trackers.yml @@ -0,0 +1,4 @@ +--- +title: Prevent more than one issue tracker to be active for the same project +merge_request: +author: luisdgs19 diff --git a/changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml b/changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml new file mode 100644 index 00000000000..66d5bb63734 --- /dev/null +++ b/changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml @@ -0,0 +1,4 @@ +--- +title: Add frequently used emojis back to awards menu +merge_request: +author: diff --git a/changelogs/unreleased/enable-snippets-by-default.yml b/changelogs/unreleased/enable-snippets-by-default.yml new file mode 100644 index 00000000000..04fa3f7bdae --- /dev/null +++ b/changelogs/unreleased/enable-snippets-by-default.yml @@ -0,0 +1,4 @@ +--- +title: Enable snippets for new projects by default +merge_request: +author: diff --git a/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml new file mode 100644 index 00000000000..6365b1a1910 --- /dev/null +++ b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml @@ -0,0 +1,4 @@ +--- +title: Fix "passed with warnings" stage status on MySQL installations +merge_request: 9802 +author: diff --git a/changelogs/unreleased/fix_updated_field_in_issues-atom.yml b/changelogs/unreleased/fix_updated_field_in_issues-atom.yml new file mode 100644 index 00000000000..414facdf779 --- /dev/null +++ b/changelogs/unreleased/fix_updated_field_in_issues-atom.yml @@ -0,0 +1,4 @@ +--- +title: Fix xml.updated field in rss/atom feeds +merge_request: 9889 +author: blackst0ne diff --git a/changelogs/unreleased/tc-fix-project-create-500.yml b/changelogs/unreleased/tc-fix-project-create-500.yml new file mode 100644 index 00000000000..1b746a41eab --- /dev/null +++ b/changelogs/unreleased/tc-fix-project-create-500.yml @@ -0,0 +1,4 @@ +--- +title: Fix for creating a project through API when import_url is nil +merge_request: 9841 +author: diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 720df0cac2d..2bc39ea3f65 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -89,7 +89,7 @@ production: &base issues: true merge_requests: true wiki: true - snippets: false + snippets: true builds: true container_registry: true @@ -441,6 +441,16 @@ production: &base shared: # path: /mnt/gitlab # Default: shared + # Gitaly settings + gitaly: + # The socket_path setting is optional and obsolete. When this is set + # GitLab assumes it can reach a Gitaly services via a Unix socket at + # this path. When this is commented out GitLab will not use Gitaly. + # + # This setting is obsolete because we expect it to be moved under + # repositories/storages in GitLab 9.1. + # + # socket_path: tmp/sockets/gitaly.socket # # 4. Advanced settings diff --git a/config/initializers/inflections.rb b/config/initializers/0_inflections.rb index d4197da3fa9..d4197da3fa9 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/0_inflections.rb diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b45d0e23080..d049ae9476f 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -221,7 +221,7 @@ Settings.gitlab['session_expire_delay'] ||= 10080 Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? -Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? +Settings.gitlab.default_projects_features['snippets'] = true if Settings.gitlab.default_projects_features['snippets'].nil? Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) diff --git a/config/initializers/fix_local_cache_middleware.rb b/config/initializers/fix_local_cache_middleware.rb new file mode 100644 index 00000000000..cb37f9ed22c --- /dev/null +++ b/config/initializers/fix_local_cache_middleware.rb @@ -0,0 +1,24 @@ +module LocalCacheRegistryCleanupWithEnsure + LocalCacheRegistry = + ActiveSupport::Cache::Strategy::LocalCache::LocalCacheRegistry + LocalStore = + ActiveSupport::Cache::Strategy::LocalCache::LocalStore + + def call(env) + LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new) + response = @app.call(env) + response[2] = ::Rack::BodyProxy.new(response[2]) do + LocalCacheRegistry.set_cache_for(local_cache_key, nil) + end + cleanup_after_response = true # ADDED THIS LINE + response + rescue Rack::Utils::InvalidParameterError + [400, {}, []] + ensure # ADDED ensure CLAUSE to cleanup when something is thrown + LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless + cleanup_after_response + end +end + +ActiveSupport::Cache::Strategy::LocalCache::Middleware + .prepend(LocalCacheRegistryCleanupWithEnsure) diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md index 463715e48ca..f6f50e2c571 100644 --- a/doc/administration/pages/source.md +++ b/doc/administration/pages/source.md @@ -17,14 +17,17 @@ Pages to the latest supported version. ## Prerequisites -Before proceeding with the Pages configuration, you will need to: - -1. Have a separate domain under which the GitLab Pages will be served. In this - document we assume that to be `example.io`. -1. Configure a **wildcard DNS record**. -1. (Optional) Have a **wildcard certificate** for that domain if you decide to - serve Pages under HTTPS. -1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md) +Before proceeding with the Pages configuration, make sure that: + +1. You have a separate domain under which GitLab Pages will be served. In + this document we assume that to be `example.io`. +1. You have configured a **wildcard DNS record** for that domain. +1. You have installed the `zip` and `unzip` packages in the same server that + GitLab is installed since they are needed to compress/uncompress the + Pages artifacts. +1. (Optional) You have a **wildcard certificate** for the Pages domain if you + decide to serve Pages (`*.example.io`) under HTTPS. +1. (Optional but recommended) You have configured and enabled the [Shared Runners][] so that your users don't have to bring their own. ### DNS configuration @@ -390,3 +393,4 @@ than GitLab to prevent XSS attacks. [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../restart_gitlab.md#installations-from-source [gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4 +[shared runners]: ../../ci/runners/README.md diff --git a/doc/development/frontend.md b/doc/development/frontend.md index 9ba820eaee5..d646de7c54a 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -16,6 +16,22 @@ minification, and compression of our assets. [jQuery][jquery] is used throughout the application's JavaScript, with [Vue.js][vue] for particularly advanced, dynamic elements. +### Architecture + +The Frontend Architect is an expert who makes high-level frontend design choices +and decides on technical standards, including coding standards, and frameworks. + +When you are assigned a new feature that requires architectural design, +make sure it is discussed with one of the Frontend Architecture Experts. + +This rule also applies if you plan to change the architecture of an existing feature. + +These decisions should be accessible to everyone, so please document it on the Merge Request. + +You can find the Frontend Architecture experts on the [team page][team-page]. + +You can find documentation about the desired architecture for a new feature built with Vue.js in [here][vue-section]. + ### Vue For more complex frontend features, we recommend using Vue.js. It shares @@ -238,8 +254,8 @@ readability. See the relevant style guides for our guidelines and for information on linting: - [SCSS][scss-style-guide] -- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related -conventions and enforce them with eslint. See [our current .eslintrc][eslistrc] +- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related +conventions and enforce them with eslint. See [our current .eslintrc][eslintrc] for specific rules and patterns. ## Testing @@ -439,3 +455,5 @@ Scenario: Developer can approve merge request [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 [airbnb-js-style-guide]: https://github.com/airbnb/javascript [eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc +[team-page]: https://about.gitlab.com/team +[vue-section]: https://docs.gitlab.com/ce/development/frontend.html#how-to-build-a-new-feature-with-vue-js diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md index ead79ba6a10..794c8eb6bfe 100644 --- a/doc/development/ux_guide/copy.md +++ b/doc/development/ux_guide/copy.md @@ -167,6 +167,15 @@ 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
+
+- Destruction buttons should be clear and always say what they are destroying.
+ E.g., `Delete page` instead of just `Delete`.
+- If the copy describes another action the user can take instead of the
+ destructive one, provide a way for them to do that as a secondary button.
+- Avoid the word `cancel` or `canceled` in the descriptive copy. It can be
+ confusing when you then see the `Cancel` button.
+
---
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 8e74970b8e9..177e1a9378b 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -288,9 +288,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 8-17-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-0-stable gitlab -**Note:** You can change `8-17-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `9-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md index 4cc8be752c4..1fe38cf8d2a 100644 --- a/doc/update/8.17-to-9.0.md +++ b/doc/update/8.17-to-9.0.md @@ -1,3 +1,162 @@ +# From 8.17 to 9.0 + +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 + +We will continue supporting Ruby < 2.3 for the time being but we recommend you +upgrade to Ruby 2.3 if you're running a source installation, as this is the same +version that ships with our Omnibus package. + +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.3.tar.gz +echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz +cd ruby-2.3.3 +./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 --location https://yarnpkg.com/install.sh | bash - +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. 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 +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-0-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-0-stable-ee +``` + +### 6. 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 + +# Install/update frontend asset dependencies +sudo -u git -H npm install --production + +# Clean up assets and cache +sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile 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). + +### 7. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +``` + +### 8. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v5.0.0 +``` + +### 9. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new 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/8-17-stable:config/gitlab.yml.example origin/9-0-stable:config/gitlab.yml.example +``` + #### Configuration changes for repository storages This version introduces a new configuration structure for repository storages. @@ -85,3 +244,78 @@ 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/9-0-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/8-17-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +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 +``` + +### 10. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 11. 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 (8.17) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.16 to 8.17](8.16-to-8.17.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/9-0-stable/config/gitlab.yml.example diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb index 0f24f9bbfde..ffbc6e17dc5 100644 --- a/lib/gitlab/etag_caching/middleware.rb +++ b/lib/gitlab/etag_caching/middleware.rb @@ -37,7 +37,7 @@ module Gitlab def get_etag(env) cache_key = env['PATH_INFO'] - store = Store.new + store = Gitlab::EtagCaching::Store.new current_value = store.get(cache_key) cached_value_present = current_value.present? diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index dc73cad93a5..eea4a91f17d 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -171,6 +171,8 @@ module Gitlab end def clean_up_restored_branches(pull_request) + return if pull_request.opened? + remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists? remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists? end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 28812fd0cb9..add7236e339 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -60,6 +60,10 @@ module Gitlab source_branch.repo.id != target_branch.repo.id end + def opened? + state == 'opened' + end + private def state diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb index 1f0d96088cf..c81dc7e30d0 100644 --- a/lib/gitlab/url_sanitizer.rb +++ b/lib/gitlab/url_sanitizer.rb @@ -9,6 +9,8 @@ module Gitlab end def self.valid?(url) + return false unless url + Addressable::URI.parse(url.strip) true diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index ecf6b6e068b..5476438b8fa 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -62,7 +62,7 @@ namespace :gitlab do ref = Shellwords.escape(args[:ref]) - migrations = `git diff #{ref}.. --name-only -- db/migrate`.lines + migrations = `git diff #{ref}.. --diff-filter=A --name-only -- db/migrate`.lines .map { |file| Rails.root.join(file.strip).to_s } .select { |file| File.file?(file) } diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index d4e0ef91856..755992069ff 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe 'New/edit issue', feature: true, js: true do + include GitlabRoutingHelper + let!(:project) { create(:project) } let!(:user) { create(:user)} let!(:user2) { create(:user)} @@ -78,6 +80,14 @@ describe 'New/edit issue', feature: true, js: true do expect(page).to have_content label2.title end end + + page.within '.issuable-meta' do + issue = Issue.find_by(title: 'title') + + expect(page).to have_text("Issue #{issue.to_reference}") + # compare paths because the host differ in test + expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue)) + end end it 'correctly updates the dropdown toggle when removing a label' do diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index 1ecdb8b5983..f8518f450dc 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe 'New/edit merge request', feature: true, js: true do + include GitlabRoutingHelper + let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } let(:fork_project) { create(:project, forked_from_project: project) } let!(:user) { create(:user)} @@ -84,6 +86,15 @@ describe 'New/edit merge request', feature: true, js: true do expect(page).to have_content label2.title end end + + page.within '.issuable-meta' do + merge_request = MergeRequest.find_by(source_branch: 'fix') + + expect(page).to have_text("Merge Request #{merge_request.to_reference}") + # compare paths because the host differ in test + expect(find_link(merge_request.to_reference)[:href]) + .to end_with(merge_request_path(merge_request)) + end end end diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb index a1643fd1f43..7c319af893b 100644 --- a/spec/features/projects/edit_spec.rb +++ b/spec/features/projects/edit_spec.rb @@ -21,36 +21,28 @@ feature 'Project edit', feature: true, js: true do expect(page).to have_selector('.merge-requests-feature', visible: false) end - it 'hides merge requests section after save' do - select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level') - - expect(page).to have_selector('.merge-requests-feature', visible: false) - - click_button 'Save changes' + context 'given project with merge_requests_disabled access level' do + let(:project) { create(:project, :merge_requests_disabled) } - wait_for_ajax - - expect(page).to have_selector('.merge-requests-feature', visible: false) + it 'hides merge requests section' do + expect(page).to have_selector('.merge-requests-feature', visible: false) + end end end context 'builds select' do - it 'hides merge requests section' do + it 'hides builds select section' do select('Disabled', from: 'project_project_feature_attributes_builds_access_level') expect(page).to have_selector('.builds-feature', visible: false) end - it 'hides merge requests section after save' do - select('Disabled', from: 'project_project_feature_attributes_builds_access_level') - - expect(page).to have_selector('.builds-feature', visible: false) + context 'given project with builds_disabled access level' do + let(:project) { create(:project, :builds_disabled) } - click_button 'Save changes' - - wait_for_ajax - - expect(page).to have_selector('.builds-feature', visible: false) + it 'hides builds select section' do + expect(page).to have_selector('.builds-feature', visible: false) + end end end end diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index dc0a62ade50..9a2978006aa 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ import promisePolyfill from 'es6-promise'; +import Cookies from 'js-cookie'; import AwardsHandler from '~/awards_handler'; promisePolyfill.polyfill(); @@ -208,8 +209,8 @@ promisePolyfill.polyfill(); expect($('[data-name=alien]').is(':visible')).toBe(true); }) .then(done) - .catch(() => { - done.fail('Failed to open and build emoji menu'); + .catch((err) => { + done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); }); @@ -232,8 +233,8 @@ promisePolyfill.polyfill(); it('should add selected emoji to awards block', function(done) { return openEmojiMenuAndAddEmoji() .then(done) - .catch(() => { - done.fail('Failed to open and build emoji menu'); + .catch((err) => { + done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); it('should remove already selected emoji', function(done) { @@ -247,7 +248,46 @@ promisePolyfill.polyfill(); }) .then(done) .catch((err) => { - done.fail('Failed to open and build emoji menu'); + done.fail(`Failed to open and build emoji menu: ${err.message}`); + }); + }); + }); + + describe('frequently used emojis', function() { + beforeEach(() => { + // Clear it out + Cookies.set('frequently_used_emojis', ''); + }); + + it('shouldn\'t have any "Frequently used" heading if no frequently used emojis', function(done) { + return openAndWaitForEmojiMenu() + .then(() => { + const emojiMenu = document.querySelector('.emoji-menu'); + Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), (title) => { + expect(title.textContent.trim().toLowerCase()).not.toBe('frequently used'); + }); + }) + .then(done) + .catch((err) => { + done.fail(`Failed to open and build emoji menu: ${err.message}`); + }); + }); + + it('should have any frequently used section when there are frequently used emojis', function(done) { + awardsHandler.addEmojiToFrequentlyUsedList('8ball'); + + return openAndWaitForEmojiMenu() + .then(() => { + const emojiMenu = document.querySelector('.emoji-menu'); + const hasFrequentlyUsedHeading = Array.prototype.some.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title => + title.textContent.trim().toLowerCase() === 'frequently used' + ); + + expect(hasFrequentlyUsedHeading).toBe(true); + }) + .then(done) + .catch((err) => { + done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); }); diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js index 7cdade01e00..e504d41d4d4 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js @@ -1,7 +1,7 @@ /* eslint-disable no-new */ -require('~/flash'); -require('~/mini_pipeline_graph_dropdown'); +import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; +import '~/flash'; (() => { describe('Mini Pipeline Graph Dropdown', () => { @@ -13,7 +13,7 @@ require('~/mini_pipeline_graph_dropdown'); describe('When is initialized', () => { it('should initialize without errors when no options are given', () => { - const miniPipelineGraph = new window.gl.MiniPipelineGraph(); + const miniPipelineGraph = new MiniPipelineGraph(); expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container'); }); @@ -21,7 +21,7 @@ require('~/mini_pipeline_graph_dropdown'); it('should set the container as the given prop', () => { const container = '.foo'; - const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container }); + const miniPipelineGraph = new MiniPipelineGraph({ container }); expect(miniPipelineGraph.container).toEqual(container); }); @@ -29,9 +29,9 @@ require('~/mini_pipeline_graph_dropdown'); describe('When dropdown is clicked', () => { it('should call getBuildsList', () => { - const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); + const getBuildsListSpy = spyOn(MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); - new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); @@ -41,11 +41,32 @@ require('~/mini_pipeline_graph_dropdown'); it('should make a request to the endpoint provided in the html', () => { const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); - new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); }); + + it('should not close when user uses cmd/ctrl + click', () => { + spyOn($, 'ajax').and.callFake(function (params) { + params.success({ + html: `<li> + <a class="mini-pipeline-graph-dropdown-item" href="#"> + <span class="ci-status-icon ci-status-icon-failed"></span> + <span class="ci-build-text">build</span> + </a> + <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a> + </li>`, + }); + }); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + + document.querySelector('.js-builds-dropdown-button').click(); + + document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); + + expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); + }); }); }); })(); diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 3f080de99dd..8b867fbe322 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -55,9 +55,6 @@ describe Gitlab::GithubImport::Importer, lib: true do allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) end - let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } - let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } - let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:label1) do double( name: 'Bug', @@ -127,32 +124,6 @@ describe Gitlab::GithubImport::Importer, lib: true do ) end - let!(:user) { create(:user, email: octocat.email) } - let(:repository) { double(id: 1, fork: false) } - let(:source_sha) { create(:commit, project: project).id } - let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) } - let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } - let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } - let(:pull_request) do - double( - number: 1347, - milestone: nil, - state: 'open', - title: 'New feature', - body: 'Please pull these awesome changes', - head: source_branch, - base: target_branch, - assignee: nil, - user: octocat, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - merged_at: nil, - url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", - labels: [double(name: 'Label #2')] - ) - end - let(:release1) do double( tag_name: 'v1.0.0', @@ -177,12 +148,14 @@ describe Gitlab::GithubImport::Importer, lib: true do ) end + subject { described_class.new(project) } + it 'returns true' do - expect(described_class.new(project).execute).to eq true + expect(subject.execute).to eq true end it 'does not raise an error' do - expect { described_class.new(project).execute }.not_to raise_error + expect { subject.execute }.not_to raise_error end it 'stores error messages' do @@ -205,15 +178,93 @@ describe Gitlab::GithubImport::Importer, lib: true do end end + shared_examples 'Gitlab::GithubImport unit-testing' do + describe '#clean_up_restored_branches' do + subject { described_class.new(project) } + + before do + allow(gh_pull_request).to receive(:source_branch_exists?).at_least(:once) { false } + allow(gh_pull_request).to receive(:target_branch_exists?).at_least(:once) { false } + end + + context 'when pull request stills open' do + let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, pull_request) } + + it 'does not remove branches' do + expect(subject).not_to receive(:remove_branch) + subject.send(:clean_up_restored_branches, gh_pull_request) + end + end + + context 'when pull request is closed' do + let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, closed_pull_request) } + + it 'does remove branches' do + expect(subject).to receive(:remove_branch).at_least(2).times + subject.send(:clean_up_restored_branches, gh_pull_request) + end + end + end + end + let(:project) { create(:project, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:credentials) { { user: 'joe' } } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + let(:repository) { double(id: 1, fork: false) } + let(:source_sha) { create(:commit, project: project).id } + let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) } + let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } + let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } + let(:pull_request) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + merged_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", + labels: [double(name: 'Label #2')] + ) + end + let(:closed_pull_request) do + double( + number: 1347, + milestone: nil, + state: 'closed', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: updated_at, + merged_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", + labels: [double(name: 'Label #2')] + ) + end + context 'when importing a GitHub project' do let(:api_root) { 'https://api.github.com' } let(:repo_root) { 'https://github.com' } + subject { described_class.new(project) } it_behaves_like 'Gitlab::GithubImport::Importer#execute' it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + it_behaves_like 'Gitlab::GithubImport unit-testing' describe '#client' do it 'instantiates a Client' do @@ -223,7 +274,7 @@ describe Gitlab::GithubImport::Importer, lib: true do {} ) - described_class.new(project).client + subject.client end end end @@ -231,6 +282,8 @@ describe Gitlab::GithubImport::Importer, lib: true do context 'when importing a Gitea project' do let(:api_root) { 'https://try.gitea.io/api/v1' } let(:repo_root) { 'https://try.gitea.io' } + subject { described_class.new(project) } + before do project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git") end @@ -239,6 +292,7 @@ describe Gitlab::GithubImport::Importer, lib: true do let(:expected_not_called) { [:import_releases] } end it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + it_behaves_like 'Gitlab::GithubImport unit-testing' describe '#client' do it 'instantiates a Client' do @@ -248,7 +302,7 @@ describe Gitlab::GithubImport::Importer, lib: true do { host: "#{repo_root}:443/foo", api_version: 'v1' } ) - described_class.new(project).client + subject.client end end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 951cbea7857..44423917944 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -306,4 +306,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347' end end + + describe '#opened?' do + let(:raw_data) { double(base_data.merge(state: 'open')) } + + it 'returns true when state is "open"' do + expect(pull_request.opened?).to be_truthy + end + end end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index 2cb74629da8..3fd361de458 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -70,4 +70,12 @@ describe Gitlab::UrlSanitizer, lib: true do expect(sanitizer.full_url).to eq('user@server:project.git') end end + + describe '.valid?' do + it 'validates url strings' do + expect(described_class.valid?(nil)).to be(false) + expect(described_class.valid?('valid@project:url.git')).to be(true) + expect(described_class.valid?('123://invalid:url')).to be(false) + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3ea62df62f2..9962c987110 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -197,6 +197,24 @@ describe Ci::Pipeline, models: true do end end end + + context 'when there is a stage with warnings' do + before do + create(:commit_status, pipeline: pipeline, + stage: 'deploy', + name: 'prod:2', + stage_idx: 2, + status: 'failed', + allow_failure: true) + end + + it 'populates stage with correct number of warnings' do + deploy_stage = pipeline.stages.third + + expect(deploy_stage).not_to receive(:statuses) + expect(deploy_stage).to have_warnings + end + end end describe '#stages_count' do diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index c4a9743a4e2..c38faf32f7d 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -170,22 +170,31 @@ describe Ci::Stage, models: true do context 'when stage has warnings' do context 'when using memoized warnings flag' do context 'when there are warnings' do - let(:stage) { build(:ci_stage, warnings: true) } + let(:stage) { build(:ci_stage, warnings: 2) } - it 'has memoized warnings' do + it 'returns true using memoized value' do expect(stage).not_to receive(:statuses) expect(stage).to have_warnings end end context 'when there are no warnings' do - let(:stage) { build(:ci_stage, warnings: false) } + let(:stage) { build(:ci_stage, warnings: 0) } - it 'has memoized warnings' do + it 'returns false using memoized value' do expect(stage).not_to receive(:statuses) expect(stage).not_to have_warnings end end + + context 'when number of warnings is not a valid value' do + let(:stage) { build(:ci_stage, warnings: true) } + + it 'calculates statuses using database queries' do + expect(stage).to receive(:statuses).and_call_original + expect(stage).not_to have_warnings + end + end end context 'when calculating warnings from statuses' do diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb new file mode 100644 index 00000000000..fbe6f344a98 --- /dev/null +++ b/spec/models/project_services/issue_tracker_service_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe IssueTrackerService, models: true do + describe 'Validations' do + let(:project) { create :project } + + describe 'only one issue tracker per project' do + let(:service) { RedmineService.new(project: project, active: true) } + + before do + create(:service, project: project, active: true, category: 'issue_tracker') + end + + context 'when service is changed manually by user' do + it 'executes the validation' do + valid = service.valid?(:manual_change) + + expect(valid).to be_falsey + expect(service.errors[:base]).to include( + 'Another issue tracker is already in use. Only one issue tracker service can be active at a time' + ) + end + end + + context 'when service is changed internally' do + it 'does not execute the validation' do + expect(service.valid?).to be_truthy + end + end + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e1dafe3aa72..9da4140f3ce 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -697,8 +697,9 @@ describe User, models: true do describe '.search_with_secondary_emails' do delegate :search_with_secondary_emails, to: :described_class - let!(:user) { create(:user) } - let!(:email) { create(:email) } + let!(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'john.doe@example.com' ) } + let!(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'albert.smith@example.com' ) } + let!(:email) { create(:email, user: another_user) } it 'returns users with a matching name' do expect(search_with_secondary_emails(user.name)).to eq([user]) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 77f79cd5bc7..b4b23617498 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -424,6 +424,14 @@ describe API::Projects, api: true do expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy end + it 'ignores import_url when it is nil' do + project = attributes_for(:project, { import_url: nil }) + + post api('/projects', user), project + + expect(response).to have_http_status(201) + end + context 'when a visibility level is restricted' do let(:project_param) { attributes_for(:project, visibility: 'public') } diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb index 97c4bfcd248..bd5cc651c2b 100644 --- a/spec/workers/authorized_projects_worker_spec.rb +++ b/spec/workers/authorized_projects_worker_spec.rb @@ -1,12 +1,10 @@ require 'spec_helper' describe AuthorizedProjectsWorker do - let(:worker) { described_class.new } + let(:project) { create(:empty_project) } describe '.bulk_perform_and_wait' do it 'schedules the ids and waits for the jobs to complete' do - project = create(:project) - project.owner.project_authorizations.delete_all described_class.bulk_perform_and_wait([[project.owner.id]]) @@ -15,20 +13,37 @@ describe AuthorizedProjectsWorker do end end + describe '.bulk_perform_async' do + it "uses it's respective sidekiq queue" do + args = [[project.owner.id]] + push_bulk_args = { + 'class' => described_class, + 'queue' => described_class.sidekiq_options['queue'], + 'args' => args + } + + expect(Sidekiq::Client).to receive(:push_bulk).with(push_bulk_args).once + + described_class.bulk_perform_async(args) + end + end + describe '#perform' do + subject { described_class.new } + it "refreshes user's authorized projects" do user = create(:user) expect_any_instance_of(User).to receive(:refresh_authorized_projects) - worker.perform(user.id) + subject.perform(user.id) end context "when the user is not found" do it "does nothing" do expect_any_instance_of(User).not_to receive(:refresh_authorized_projects) - described_class.new.perform(-1) + subject.perform(-1) end end end diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml index 8c590579934..40648bcd3de 100644 --- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml @@ -7,7 +7,7 @@ services: build: stage: build script: - - export IMAGE_TAG=$(echo -en $CI_BUILD_REF_NAME | tr -c '[:alnum:]_.-' '-') - - docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" $CI_REGISTRY + - export IMAGE_TAG=$(echo -en $CI_COMMIT_REF_NAME | tr -c '[:alnum:]_.-' '-') + - docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" $CI_REGISTRY - docker build --pull -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" . - docker push "$CI_REGISTRY_IMAGE:$IMAGE_TAG" diff --git a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml index b75f0665bee..91b096654d1 100644 --- a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml @@ -3,9 +3,9 @@ # For docker image tags see https://hub.docker.com/_/maven/ # # For general lifecycle information see https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html -# +# # This template will build and test your projects as well as create the documentation. -# +# # * Caches downloaded dependencies and plugins between invocation. # * Does only verify merge requests but deploy built artifacts of the # master branch. @@ -24,12 +24,12 @@ variables: MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true" # Cache downloaded dependencies and plugins between builds. -# To keep cache across branches add 'key: "$CI_BUILD_REF_NAME"' +# To keep cache across branches add 'key: "$CI_JOB_REF_NAME"' cache: paths: - .m2/repository -# This will only validate and compile stuff and run e.g. maven-enforcer-plugin. +# This will only validate and compile stuff and run e.g. maven-enforcer-plugin. # Because some enforcer rules might check dependency convergence and class duplications # we use `test-compile` here instead of `validate`, so the correct classpath is picked up. .validate: &validate diff --git a/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml index 6b6c405a507..d3bb388a1e7 100644 --- a/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml @@ -38,11 +38,11 @@ review: <<: *deploy stage: review variables: - APP: $CI_BUILD_REF_NAME - APP_HOST: $CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN + APP: $CI_COMMIT_REF_NAME + APP_HOST: $CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN environment: - name: review/$CI_BUILD_REF_NAME - url: http://$CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN + name: review/$CI_COMMIT_REF_SLUG + url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN on_stop: stop-review only: - branches @@ -56,10 +56,10 @@ stop-review: - oc delete all -l "app=$APP" when: manual variables: - APP: $CI_BUILD_REF_NAME + APP: $CI_COMMIT_REF_NAME GIT_STRATEGY: none environment: - name: review/$CI_BUILD_REF_NAME + name: review/$CI_COMMIT_REF_SLUG action: stop only: - branches diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml index 574f9365f14..c644560647f 100644 --- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml @@ -24,12 +24,12 @@ build: production: stage: production variables: - CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN + CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME.$KUBE_DOMAIN script: - command deploy environment: name: production - url: http://production.$KUBE_DOMAIN + url: http://$CI_PROJECT_NAME.$KUBE_DOMAIN when: manual only: - master @@ -37,24 +37,24 @@ production: staging: stage: staging variables: - CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN + CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN script: - command deploy environment: name: staging - url: http://staging.$KUBE_DOMAIN + url: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN only: - master review: stage: review variables: - CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN + CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN script: - command deploy environment: - name: review/$CI_BUILD_REF_NAME - url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN + name: review/$CI_COMMIT_REF_NAME + url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN on_stop: stop_review only: - branches @@ -68,7 +68,7 @@ stop_review: script: - command destroy environment: - name: review/$CI_BUILD_REF_NAME + name: review/$CI_COMMIT_REF_NAME action: stop when: manual only: diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml index 4d6f4e00ebb..27c9107e0d7 100644 --- a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml @@ -1,4 +1,4 @@ -# Explaination on the scripts: +# Explanation on the scripts: # https://gitlab.com/gitlab-examples/openshift-deploy/blob/master/README.md image: registry.gitlab.com/gitlab-examples/openshift-deploy @@ -24,12 +24,12 @@ build: production: stage: production variables: - CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN + CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME.$KUBE_DOMAIN script: - command deploy environment: name: production - url: http://production.$KUBE_DOMAIN + url: http://$CI_PROJECT_NAME.$KUBE_DOMAIN when: manual only: - master @@ -37,24 +37,24 @@ production: staging: stage: staging variables: - CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN + CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN script: - command deploy environment: name: staging - url: http://staging.$KUBE_DOMAIN + url: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN only: - master review: stage: review variables: - CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN + CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN script: - command deploy environment: - name: review/$CI_BUILD_REF_NAME - url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN + name: review/$CI_COMMIT_REF_NAME + url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN on_stop: stop_review only: - branches @@ -68,7 +68,7 @@ stop_review: script: - command destroy environment: - name: review/$CI_BUILD_REF_NAME + name: review/$CI_COMMIT_REF_NAME action: stop when: manual only: |