diff options
47 files changed, 770 insertions, 206 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 436e9ec6c60..bb94fc41ad1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -319,12 +319,11 @@ migration paths: - master@gitlab/gitlabhq - master@gitlab/gitlab-ee script: - - git checkout HEAD . - - git fetch --tags - - git checkout v8.5.9 + - git fetch origin v8.5.9 + - git checkout -f FETCH_HEAD - cp config/resque.yml.example config/resque.yml - sed -i 's/localhost/redis/g' config/resque.yml - - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3 + - bundle install --without postgres production --jobs $(nproc) ${FLAGS[@]} --retry=3 - rake db:drop db:create db:schema:load db:seed_fu - git checkout $CI_BUILD_REF - source scripts/prepare_build.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f41cbc9228..5b139ac8314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ entry. - Fix sidekiq stats in admin area (blackst0ne) - Added label description as tooltip to issue board list title - Created cycle analytics bundle JavaScript file +- Make the milestone page more responsive (yury-n) - Hides container registry when repository is disabled - API: Fix booleans not recognized as such when using the `to_boolean` helper - Removed delete branch tooltip !6954 diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js deleted file mode 100644 index 906a1a69d93..00000000000 --- a/app/assets/javascripts/activities.js +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, quotes, no-var, padded-blocks, max-len */ -(function() { - this.Activities = (function() { - function Activities() { - Pager.init(20, true, false, this.updateTooltips); - $(".event-filter-link").on("click", (function(_this) { - return function(event) { - event.preventDefault(); - _this.toggleFilter($(event.currentTarget)); - return _this.reloadActivities(); - }; - })(this)); - } - - Activities.prototype.updateTooltips = function() { - gl.utils.localTimeAgo($('.js-timeago', '.content_list')); - }; - - Activities.prototype.reloadActivities = function() { - $(".content_list").html(''); - Pager.init(20, true, false, this.updateTooltips); - }; - - Activities.prototype.toggleFilter = function(sender) { - var filter = sender.attr("id").split("_")[0]; - - $('.event-filter .active').removeClass("active"); - Cookies.set("event_filter", filter); - - sender.closest('li').toggleClass("active"); - }; - - return Activities; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/activities.js.es6 b/app/assets/javascripts/activities.js.es6 new file mode 100644 index 00000000000..19bcfef89fb --- /dev/null +++ b/app/assets/javascripts/activities.js.es6 @@ -0,0 +1,36 @@ +/* eslint-disable no-param-reassign, class-methods-use-this */ +/* global Pager, Cookies */ + +((global) => { + class Activities { + constructor() { + Pager.init(20, true, false, this.updateTooltips); + $('.event-filter-link').on('click', (e) => { + e.preventDefault(); + this.toggleFilter(e.currentTarget); + this.reloadActivities(); + }); + } + + updateTooltips() { + gl.utils.localTimeAgo($('.js-timeago', '.content_list')); + } + + reloadActivities() { + $('.content_list').html(''); + Pager.init(20, true, false, this.updateTooltips); + } + + toggleFilter(sender) { + const $sender = $(sender); + const filter = $sender.attr('id').split('_')[0]; + + $('.event-filter .active').removeClass('active'); + Cookies.set('event_filter', filter); + + $sender.closest('li').toggleClass('active'); + } + } + + global.Activities = Activities; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 2867b3d1c28..c2d4670b7e9 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -110,10 +110,10 @@ Issuable.init(); break; case 'dashboard:activity': - new Activities(); + new gl.Activities(); break; case 'dashboard:projects:starred': - new Activities(); + new gl.Activities(); break; case 'projects:commit:show': new Commit(); @@ -139,7 +139,7 @@ new gl.Pipelines(); break; case 'groups:activity': - new Activities(); + new gl.Activities(); break; case 'groups:show': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 952110744d4..1043e516483 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -232,7 +232,9 @@ is="environment-item" v-for="children in model.children" :model="children" - :toggleRow="toggleRow.bind(children)"> + :toggleRow="toggleRow.bind(children)" + :can-create-deployment="canCreateDeploymentParsed" + :can-read-environment="canReadEnvironmentParsed"> </tr> </template> diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js deleted file mode 100644 index d22d2d9dbae..00000000000 --- a/app/assets/javascripts/pager.js +++ /dev/null @@ -1,64 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-undef, prefer-template, wrap-iife, comma-dangle, no-return-assign, no-else-return, consistent-return, no-unused-vars, padded-blocks, max-len */ -(function() { - this.Pager = { - init: function(limit, preload, disable, callback) { - this.limit = limit != null ? limit : 0; - this.disable = disable != null ? disable : false; - this.callback = callback != null ? callback : $.noop; - this.loading = $('.loading').first(); - if (preload) { - this.offset = 0; - this.getOld(); - } else { - this.offset = this.limit; - } - return this.initLoadMore(); - }, - getOld: function() { - this.loading.show(); - return $.ajax({ - type: "GET", - url: $(".content_list").data('href') || location.href, - data: "limit=" + this.limit + "&offset=" + this.offset, - complete: (function(_this) { - return function() { - return _this.loading.hide(); - }; - })(this), - success: function(data) { - Pager.append(data.count, data.html); - return Pager.callback(); - }, - dataType: "json" - }); - }, - append: function(count, html) { - $(".content_list").append(html); - if (count > 0) { - return this.offset += count; - } else { - return this.disable = true; - } - }, - initLoadMore: function() { - $(document).unbind('scroll'); - return $(document).endlessScroll({ - bottomPixels: 400, - fireDelay: 1000, - fireOnce: true, - ceaseFire: function() { - return Pager.disable; - }, - callback: (function(_this) { - return function(i) { - if (!_this.loading.is(':visible')) { - _this.loading.show(); - return Pager.getOld(); - } - }; - })(this) - }); - } - }; - -}).call(this); diff --git a/app/assets/javascripts/pager.js.es6 b/app/assets/javascripts/pager.js.es6 new file mode 100644 index 00000000000..e35cf6d295e --- /dev/null +++ b/app/assets/javascripts/pager.js.es6 @@ -0,0 +1,73 @@ +(() => { + const ENDLESS_SCROLL_BOTTOM_PX = 400; + const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; + + const Pager = { + init(limit = 0, preload = false, disable = false, callback = $.noop) { + this.limit = limit; + this.offset = this.limit; + this.disable = disable; + this.callback = callback; + this.loading = $('.loading').first(); + if (preload) { + this.offset = 0; + this.getOld(); + } + this.initLoadMore(); + }, + + getOld() { + this.loading.show(); + $.ajax({ + type: 'GET', + url: $('.content_list').data('href') || window.location.href, + data: `limit=${this.limit}&offset=${this.offset}`, + dataType: 'json', + error: () => this.loading.hide(), + success: (data) => { + this.append(data.count, data.html); + this.callback(); + + // keep loading until we've filled the viewport height + if (!this.disable && !this.isScrollable()) { + this.getOld(); + } else { + this.loading.hide(); + } + }, + }); + }, + + append(count, html) { + $('.content_list').append(html); + if (count > 0) { + this.offset += count; + } else { + this.disable = true; + } + }, + + isScrollable() { + const $w = $(window); + return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX; + }, + + initLoadMore() { + $(document).unbind('scroll'); + $(document).endlessScroll({ + bottomPixels: ENDLESS_SCROLL_BOTTOM_PX, + fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS, + fireOnce: true, + ceaseFire: () => this.disable === true, + callback: () => { + if (!this.loading.is(':visible')) { + this.loading.show(); + this.getOld(); + } + }, + }); + }, + }; + + window.Pager = Pager; +})(); diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 index 2b310da319c..5a625611987 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -134,7 +134,7 @@ content on the Users#show page. } const $calendarWrap = this.$parentEl.find('.user-calendar'); $calendarWrap.load($calendarWrap.data('href')); - new Activities(); + new gl.Activities(); return this.loaded['activity'] = true; } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 13402acd8e1..8843d1463db 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -11,6 +11,7 @@ } .progress { + width: 100%; height: 6px; } } @@ -30,7 +31,6 @@ margin-right: 7px; } - // Issue title span a { color: $gl-text-color; word-wrap: break-word; @@ -39,15 +39,66 @@ } .milestone-summary { - margin-bottom: 25px; - .milestone-stat { + white-space: nowrap; margin-right: 10px; + + &.with-drilldown { + margin-right: 2px; + } } .remaining-days { color: $orange-light; } + + .milestone-stats-and-buttons { + display: flex; + justify-content: flex-start; + flex-wrap: wrap; + + @media (min-width: $screen-xs-min) { + justify-content: space-between; + flex-wrap: nowrap; + } + } + + .milestone-progress-buttons { + order: 1; + margin-top: 10px; + + @media (min-width: $screen-xs-min) { + order: 2; + margin-top: 0; + flex-shrink: 0; + } + + .btn { + float: left; + margin-right: $btn-side-margin; + + &:last-child { + margin-right: 0; + } + } + } + + .milestone-stats { + order: 2; + width: 100%; + padding: 7px 0; + flex-shrink: 1; + + @media (min-width: $screen-xs-min) { + // when displayed on one line stats go first, buttons second + order: 1; + } + } + + .progress { + width: 100%; + margin: 15px 0; + } } .issues-sortable-list, @@ -82,3 +133,50 @@ } } } + +.milestone-page-header { + display: flex; + flex-flow: row; + align-items: center; + flex-wrap: wrap; + + .status-box { + margin-top: 0; + } + + .milestone-buttons { + margin-left: auto; + } + + .status-box { + order: 1; + } + + .milestone-buttons { + order: 2; + } + + .header-text-content { + order: 3; + width: 100%; + } + + .milestone-buttons .verbose { + display: none; + } + + @media (min-width: $screen-xs-min) { + .milestone-buttons .verbose { + display: inline; + } + + .header-text-content { + order: 2; + width: auto; + } + + .milestone-buttons { + order: 3; + } + } +} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index b343ba0b744..dbbd2ad849e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -82,12 +82,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = if params[:diff_id] - @merge_request.merge_request_diffs.find(params[:diff_id]) + @merge_request.merge_request_diffs.viewable.find(params[:diff_id]) else @merge_request.merge_request_diff end - @merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff + @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } if params[:start_sha].present? @@ -417,7 +417,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController response = { title: merge_request.title, - sha: merge_request.diff_head_commit.short_id, + sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha), status: status, coverage: coverage } @@ -564,7 +564,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_pipelines_vars @pipelines = @merge_request.all_pipelines - if @pipelines.present? + if @pipelines.present? && @merge_request.commits.present? @pipeline = @pipelines.first @statuses = @pipeline.statuses.relevant end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 83c0c64e5fb..e7d33bd26db 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -487,6 +487,10 @@ module Ci ] end + def credentials + Gitlab::Ci::Build::Credentials::Factory.new(self).create! + end + private def update_artifacts_size diff --git a/app/models/environment.rb b/app/models/environment.rb index 5278efd71d2..a7f4156fc2e 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -19,7 +19,7 @@ class Environment < ActiveRecord::Base allow_nil: true, addressable_url: true - delegate :stop_action, to: :last_deployment, allow_nil: true + delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true scope :available, -> { with_state(:available) } scope :stopped, -> { with_state(:stopped) } @@ -99,4 +99,12 @@ class Environment < ActiveRecord::Base stop stop_action.play(current_user) end + + def actions_for(environment) + return [] unless manual_actions + + manual_actions.select do |action| + action.expanded_environment_name == environment + end + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index dd65a9a8b86..58a24eb84cb 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base belongs_to :merge_request + serialize :st_commits + serialize :st_diffs + state_machine :state, initial: :empty do state :collected state :overflow @@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base state :overflow_diff_lines_limit end - serialize :st_commits - serialize :st_diffs + scope :viewable, -> { without_state(:empty) } # All diff information is collected from repository after object is created. # It allows you to override variables like head_commit_sha before getting diff. diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 4a7e6930842..22596b4014a 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -60,7 +60,15 @@ module MergeRequests merge_requests = filter_merge_requests(merge_requests) merge_requests.each do |merge_request| - reload_diff(merge_request) unless branch_removed? + if merge_request.source_branch == @branch_name || force_push? + merge_request.reload_diff + else + mr_commit_ids = merge_request.commits.map(&:id) + push_commit_ids = @commits.map(&:id) + matches = mr_commit_ids & push_commit_ids + merge_request.reload_diff if matches.any? + end + merge_request.mark_as_unchecked end end @@ -165,16 +173,5 @@ module MergeRequests def branch_removed? Gitlab::Git.blank_ref?(@newrev) end - - def reload_diff(merge_request) - if merge_request.source_branch == @branch_name || force_push? - merge_request.reload_diff - else - mr_commit_ids = merge_request.commits.map(&:id) - push_commit_ids = @commits.map(&:id) - matches = mr_commit_ids & push_commit_ids - merge_request.reload_diff if matches.any? - end - end end end diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml index 6b32d377e1a..1ec1e7c70e4 100644 --- a/app/views/profiles/chat_names/_chat_name.html.haml +++ b/app/views/profiles/chat_names/_chat_name.html.haml @@ -19,7 +19,7 @@ = chat_name.chat_name %td - if chat_name.last_used_at - time_ago_with_tooltip(chat_name.last_used_at) + = time_ago_with_tooltip(chat_name.last_used_at) - else Never diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index d011e51e696..4f15f2997fb 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -13,7 +13,7 @@ = spinner :javascript - var activity = new Activities(); + var activity = new gl.Activities(); $(document).on('page:restore', function (event) { activity.reloadActivities() }) diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index ac26aa569ba..20c93930abc 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -9,10 +9,10 @@ - if @project.archived? = render 'projects/merge_requests/widget/open/archived' - - elsif @merge_request.commits.blank? - = render 'projects/merge_requests/widget/open/nothing' - elsif @merge_request.branch_missing? = render 'projects/merge_requests/widget/open/missing_branch' + - elsif @merge_request.commits.blank? + = render 'projects/merge_requests/widget/open/nothing' - elsif @merge_request.unchecked? = render 'projects/merge_requests/widget/open/check' - elsif @merge_request.cannot_be_merged? && !resolved_conflicts diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index f9ba77e87b5..e01aca3dda6 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -4,7 +4,7 @@ = render "projects/issues/head" %div{ class: container_class } - .detail-page-header + .detail-page-header.milestone-page-header .status-box{ class: status_box_class(@milestone) } - if @milestone.closed? Closed @@ -12,13 +12,14 @@ Past due - else Open - %span.identifier - Milestone ##{@milestone.iid} - - if @milestone.expires_at - %span.creator - · - = @milestone.expires_at - .pull-right + .header-text-content + %span.identifier + Milestone ##{@milestone.iid} + - if @milestone.expires_at + %span.creator + · + = @milestone.expires_at + .milestone-buttons - if can?(current_user, :admin_milestone, @project) - if @milestone.active? = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml index dee2472fa79..0a237136959 100644 --- a/app/views/shared/milestones/_summary.html.haml +++ b/app/views/shared/milestones/_summary.html.haml @@ -3,32 +3,38 @@ .context.prepend-top-default .milestone-summary %h4 Progress - %strong= milestone.issues_visible_to_user(current_user).size - issues: - %span.milestone-stat - %strong= milestone.issues_visible_to_user(current_user).opened.size - open and - %strong= milestone.issues_visible_to_user(current_user).closed.size - closed - %strong= milestone.merge_requests.size - merge requests: - %span.milestone-stat - %strong= milestone.merge_requests.opened.size - open and - %strong= milestone.merge_requests.merged.size - merged - %span.milestone-stat - %strong== #{milestone.percent_complete(current_user)}% - complete - %span.milestone-stat - %span.remaining-days= milestone_remaining_days(milestone) - %span.pull-right.tab-issues-buttons - - if project && can?(current_user, :create_issue, project) - = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do - New Issue - = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped" - %span.pull-right.tab-merge-requests-buttons.hidden - = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped" + .milestone-stats-and-buttons + .milestone-stats + %span.milestone-stat.with-drilldown + %strong= milestone.issues_visible_to_user(current_user).size + issues: + %span.milestone-stat + %strong= milestone.issues_visible_to_user(current_user).opened.size + open and + %strong= milestone.issues_visible_to_user(current_user).closed.size + closed + %span.milestone-stat.with-drilldown + %strong= milestone.merge_requests.size + merge requests: + %span.milestone-stat + %strong= milestone.merge_requests.opened.size + open and + %strong= milestone.merge_requests.merged.size + merged + %span.milestone-stat + %strong== #{milestone.percent_complete(current_user)}% + complete + %span.milestone-stat + %span.remaining-days= milestone_remaining_days(milestone) - = milestone_progress_bar(milestone) + .milestone-progress-buttons + %span.tab-issues-buttons + - if project && can?(current_user, :create_issue, project) + = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do + New Issue + = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn" + %span.tab-merge-requests-buttons.hidden + = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn" + + = milestone_progress_bar(milestone) diff --git a/changelogs/unreleased/24499-fix-activity-autoload-on-large-viewports.yml b/changelogs/unreleased/24499-fix-activity-autoload-on-large-viewports.yml new file mode 100644 index 00000000000..53dcc2a82f1 --- /dev/null +++ b/changelogs/unreleased/24499-fix-activity-autoload-on-large-viewports.yml @@ -0,0 +1,4 @@ +--- +title: Fix activity page endless scroll on large viewports +merge_request: 7608 +author: diff --git a/changelogs/unreleased/chatops-deploy-command.yml b/changelogs/unreleased/chatops-deploy-command.yml new file mode 100644 index 00000000000..1e5a3e8df15 --- /dev/null +++ b/changelogs/unreleased/chatops-deploy-command.yml @@ -0,0 +1,4 @@ +--- +title: Add deployment command to ChatOps +merge_request: 7619 +author: diff --git a/changelogs/unreleased/dz-fix-500-group-git.yml b/changelogs/unreleased/dz-fix-500-group-git.yml new file mode 100644 index 00000000000..38e80ad8bde --- /dev/null +++ b/changelogs/unreleased/dz-fix-500-group-git.yml @@ -0,0 +1,4 @@ +--- +title: Fix 500 error when group name ends with git +merge_request: 7630 +author: diff --git a/changelogs/unreleased/feature-send-registry-address-with-build-payload.yml b/changelogs/unreleased/feature-send-registry-address-with-build-payload.yml new file mode 100644 index 00000000000..db9bb2bc31f --- /dev/null +++ b/changelogs/unreleased/feature-send-registry-address-with-build-payload.yml @@ -0,0 +1,4 @@ +--- +title: Send credentials (currently for registry only) with build data to GitLab Runner +merge_request: 7474 +author: diff --git a/changelogs/unreleased/fix-merge-request-screen-deleted-source-branch.yml b/changelogs/unreleased/fix-merge-request-screen-deleted-source-branch.yml deleted file mode 100644 index a6bee989f6d..00000000000 --- a/changelogs/unreleased/fix-merge-request-screen-deleted-source-branch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Do not create a MergeRequestDiff record when source branch is deleted -merge_request: 7481 -author: diff --git a/changelogs/unreleased/hide-empty-merge-request-diffs.yml b/changelogs/unreleased/hide-empty-merge-request-diffs.yml new file mode 100644 index 00000000000..e32a51b555a --- /dev/null +++ b/changelogs/unreleased/hide-empty-merge-request-diffs.yml @@ -0,0 +1,4 @@ +--- +title: Fix errors happening when source branch of merge request is removed and then restored +merge_request: 7568 +author: diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index 66c05773b68..792ff628b09 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -32,6 +32,10 @@ module Ci expose :artifacts_file, using: ArtifactFile, if: ->(build, _) { build.artifacts? } end + class BuildCredentials < Grape::Entity + expose :type, :url, :username, :password + end + class BuildDetails < Build expose :commands expose :repo_url @@ -50,6 +54,8 @@ module Ci expose :variables expose :depends_on_builds, using: Build + + expose :credentials, using: BuildCredentials end class Runner < Grape::Entity diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 5f131703d40..0ec358debc7 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -4,6 +4,7 @@ module Gitlab COMMANDS = [ Gitlab::ChatCommands::IssueShow, Gitlab::ChatCommands::IssueCreate, + Gitlab::ChatCommands::Deploy, ].freeze def execute diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb new file mode 100644 index 00000000000..0eed1fce0dc --- /dev/null +++ b/lib/gitlab/chat_commands/deploy.rb @@ -0,0 +1,57 @@ +module Gitlab + module ChatCommands + class Deploy < BaseCommand + include Gitlab::Routing.url_helpers + + def self.match(text) + /\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text) + end + + def self.help_message + 'deploy <environment> to <target-environment>' + end + + def self.available?(project) + project.builds_enabled? + end + + def self.allowed?(project, user) + can?(user, :create_deployment, project) + end + + def execute(match) + from = match[:from] + to = match[:to] + + actions = find_actions(from, to) + return unless actions.present? + + if actions.one? + play!(from, to, actions.first) + else + Result.new(:error, 'Too many actions defined') + end + end + + private + + def play!(from, to, action) + new_action = action.play(current_user) + + Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.") + end + + def find_actions(from, to) + environment = project.environments.find_by(name: from) + return unless environment + + environment.actions_for(to).select(&:starts_environment?) + end + + def url(subject) + polymorphic_url( + [ subject.project.namespace.becomes(Namespace), subject.project, subject ]) + end + end + end +end diff --git a/lib/gitlab/chat_commands/result.rb b/lib/gitlab/chat_commands/result.rb new file mode 100644 index 00000000000..324d7ef43a3 --- /dev/null +++ b/lib/gitlab/chat_commands/result.rb @@ -0,0 +1,5 @@ +module Gitlab + module ChatCommands + Result = Struct.new(:type, :message) + end +end diff --git a/lib/gitlab/ci/build/credentials/base.rb b/lib/gitlab/ci/build/credentials/base.rb new file mode 100644 index 00000000000..29a7a27c963 --- /dev/null +++ b/lib/gitlab/ci/build/credentials/base.rb @@ -0,0 +1,13 @@ +module Gitlab + module Ci + module Build + module Credentials + class Base + def type + self.class.name.demodulize.underscore + end + end + end + end + end +end diff --git a/lib/gitlab/ci/build/credentials/factory.rb b/lib/gitlab/ci/build/credentials/factory.rb new file mode 100644 index 00000000000..2423aa8857d --- /dev/null +++ b/lib/gitlab/ci/build/credentials/factory.rb @@ -0,0 +1,27 @@ +module Gitlab + module Ci + module Build + module Credentials + class Factory + def initialize(build) + @build = build + end + + def create! + credentials.select(&:valid?) + end + + private + + def credentials + providers.map { |provider| provider.new(@build) } + end + + def providers + [Registry] + end + end + end + end + end +end diff --git a/lib/gitlab/ci/build/credentials/registry.rb b/lib/gitlab/ci/build/credentials/registry.rb new file mode 100644 index 00000000000..55eafcaed10 --- /dev/null +++ b/lib/gitlab/ci/build/credentials/registry.rb @@ -0,0 +1,24 @@ +module Gitlab + module Ci + module Build + module Credentials + class Registry < Base + attr_reader :username, :password + + def initialize(build) + @username = 'gitlab-ci-token' + @password = build.token + end + + def url + Gitlab.config.registry.host_port + end + + def valid? + Gitlab.config.registry.enabled + end + end + end + end + end +end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 47ea8b7e82e..c12358ceef4 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -9,7 +9,7 @@ module Gitlab # `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation. NAMESPACE_REGEX_STR_SIMPLE = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze - NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR_SIMPLE})(?<!\.git|\.atom)".freeze + NAMESPACE_REGEX_STR = '(?:' + NAMESPACE_REGEX_STR_SIMPLE + ')(?<!\.git|\.atom)'.freeze def namespace_regex @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb index bfbb089eb02..67eda983a74 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/mattermost/presenter.rb @@ -24,20 +24,22 @@ module Mattermost end end - def present(resource) - return not_found unless resource - - if resource.respond_to?(:count) - if resource.count > 1 - return multiple_resources(resource) - elsif resource.count == 0 - return not_found + def present(subject) + return not_found unless subject + + if subject.is_a?(Gitlab::ChatCommands::Result) + show_result(subject) + elsif subject.respond_to?(:count) + if subject.many? + multiple_resources(subject) + elsif subject.none? + not_found else - resource = resource.first + single_resource(subject) end + else + single_resource(subject) end - - single_resource(resource) end def access_denied @@ -46,6 +48,15 @@ module Mattermost private + def show_result(result) + case result.type + when :success + in_channel_response(result.message) + else + ephemeral_response(result.message) + end + end + def not_found ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") end @@ -54,7 +65,7 @@ module Mattermost return error(resource) if resource.errors.any? || !resource.persisted? message = "### #{title(resource)}" - message << "\n\n#{resource.description}" if resource.description + message << "\n\n#{resource.description}" if resource.try(:description) in_channel_response(message) end @@ -74,7 +85,10 @@ module Mattermost end def title(resource) - "[#{resource.to_reference} #{resource.title}](#{url(resource)})" + reference = resource.try(:to_reference) || resource.try(:id) + title = resource.try(:title) || resource.try(:name) + + "[#{reference} #{title}](#{url(resource)})" end def header_with_list(header, items) diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 0c93bbdfe26..eb20bd7dd58 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -55,6 +55,12 @@ FactoryGirl.define do self.when 'manual' end + trait :teardown_environment do + options do + { environment: { action: 'stop' } } + end + end + trait :allowed_to_fail do allow_failure true end diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb new file mode 100644 index 00000000000..778b3a90cf3 --- /dev/null +++ b/spec/features/merge_requests/deleted_source_branch_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe 'Deleted source branch', feature: true, js: true do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + + before do + login_as user + merge_request.project.team << [user, :master] + merge_request.update!(source_branch: 'this-branch-does-not-exist') + visit namespace_project_merge_request_path( + merge_request.project.namespace, + merge_request.project, merge_request + ) + end + + it 'shows a message about missing source branch' do + expect(page).to have_content( + 'Source branch this-branch-does-not-exist does not exist' + ) + end + + it 'hides Discussion, Commits and Changes tabs' do + within '.merge-request-details' do + expect(page).to have_no_content('Discussion') + expect(page).to have_no_content('Commits') + expect(page).to have_no_content('Changes') + end + end +end diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb index 23cee891bac..09451f41de4 100644 --- a/spec/features/merge_requests/merge_request_versions_spec.rb +++ b/spec/features/merge_requests/merge_request_versions_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' feature 'Merge Request versions', js: true, feature: true do let(:merge_request) { create(:merge_request, importing: true) } let(:project) { merge_request.source_project } + let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } + let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) } + let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') } before do login_as :admin - merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') - merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) end @@ -53,7 +54,7 @@ feature 'Merge Request versions', js: true, feature: true do project.namespace, project, merge_request.iid, - diff_id: 2, + diff_id: merge_request_diff3.id, start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' ) end diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 9d855ef1060..8640cd44085 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -35,7 +35,7 @@ describe('Activities', () => { beforeEach(() => { fixture.load(fixtureTemplate); - new Activities(); + new gl.Activities(); }); for(let i = 0; i < filters.length; i++) { diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index 8cedbb0240f..bfc6818ac08 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -4,9 +4,9 @@ describe Gitlab::ChatCommands::Command, service: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } - subject { described_class.new(project, user, params).execute } - describe '#execute' do + subject { described_class.new(project, user, params).execute } + context 'when no command is available' do let(:params) { { text: 'issue show 1' } } let(:project) { create(:project, has_external_issue_tracker: true) } @@ -51,5 +51,44 @@ describe Gitlab::ChatCommands::Command, service: true do expect(subject[:text]).to match(/\/issues\/\d+/) end end + + context 'when trying to do deployment' do + let(:params) { { text: 'deploy staging to production' } } + let!(:build) { create(:ci_build, project: project) } + let!(:staging) { create(:environment, name: 'staging', project: project) } + let!(:deployment) { create(:deployment, environment: staging, deployable: build) } + let!(:manual) do + create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production') + end + + context 'and user can not create deployment' do + it 'returns action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to start_with('Whoops! That action is not allowed') + end + end + + context 'and user does have deployment permission' do + before do + project.team << [user, :developer] + end + + it 'returns action' do + expect(subject[:text]).to include('Deployment from staging to production started') + expect(subject[:response_type]).to be(:in_channel) + end + + context 'when duplicate action exists' do + let!(:manual2) do + create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production') + end + + it 'returns error' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to include('Too many actions defined') + end + end + end + end end end diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb new file mode 100644 index 00000000000..bd8099c92da --- /dev/null +++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Deploy, service: true do + describe '#execute' do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:regex_match) { described_class.match('deploy staging to production') } + + before do + project.team << [user, :master] + end + + subject do + described_class.new(project, user).execute(regex_match) + end + + context 'if no environment is defined' do + it 'returns nil' do + expect(subject).to be_nil + end + end + + context 'with environment' do + let!(:staging) { create(:environment, name: 'staging', project: project) } + let!(:build) { create(:ci_build, project: project) } + let!(:deployment) { create(:deployment, environment: staging, deployable: build) } + + context 'without actions' do + it 'returns nil' do + expect(subject).to be_nil + end + end + + context 'with action' do + let!(:manual1) do + create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production') + end + + it 'returns success result' do + expect(subject.type).to eq(:success) + expect(subject.message).to include('Deployment from staging to production started') + end + + context 'when duplicate action exists' do + let!(:manual2) do + create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production') + end + + it 'returns error' do + expect(subject.type).to eq(:error) + expect(subject.message).to include('Too many actions defined') + end + end + + context 'when teardown action exists' do + let!(:teardown) do + create(:ci_build, :manual, :teardown_environment, + project: project, pipeline: build.pipeline, + name: 'teardown', environment: 'production') + end + + it 'returns success result' do + expect(subject.type).to eq(:success) + expect(subject.message).to include('Deployment from staging to production started') + end + end + end + end + end + + describe 'self.match' do + it 'matches the environment' do + match = described_class.match('deploy staging to production') + + expect(match[:from]).to eq('staging') + expect(match[:to]).to eq('production') + end + end +end diff --git a/spec/lib/gitlab/ci/build/credentials/factory_spec.rb b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb new file mode 100644 index 00000000000..10b4b7a8826 --- /dev/null +++ b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Credentials::Factory do + let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) } + + subject { Gitlab::Ci::Build::Credentials::Factory.new(build).create! } + + class TestProvider + def initialize(build); end + end + + before do + allow_any_instance_of(Gitlab::Ci::Build::Credentials::Factory).to receive(:providers).and_return([TestProvider]) + end + + context 'when provider is valid' do + before do + allow_any_instance_of(TestProvider).to receive(:valid?).and_return(true) + end + + it 'generates an array of credentials objects' do + is_expected.to be_kind_of(Array) + is_expected.not_to be_empty + expect(subject.first).to be_kind_of(TestProvider) + end + end + + context 'when provider is not valid' do + before do + allow_any_instance_of(TestProvider).to receive(:valid?).and_return(false) + end + + it 'generates an array without specific credential object' do + is_expected.to be_kind_of(Array) + is_expected.to be_empty + end + end +end diff --git a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb new file mode 100644 index 00000000000..84e44dd53e2 --- /dev/null +++ b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Credentials::Registry do + let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) } + let(:registry_url) { 'registry.example.com:5005' } + + subject { Gitlab::Ci::Build::Credentials::Registry.new(build) } + + before do + stub_container_registry_config(host_port: registry_url) + end + + it 'contains valid DockerRegistry credentials' do + expect(subject).to be_kind_of(Gitlab::Ci::Build::Credentials::Registry) + + expect(subject.username).to eq 'gitlab-ci-token' + expect(subject.password).to eq build.token + expect(subject.url).to eq registry_url + expect(subject.type).to eq 'registry' + end + + describe '.valid?' do + subject { Gitlab::Ci::Build::Credentials::Registry.new(build).valid? } + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it { is_expected.to be_truthy } + end + + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false) + end + + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 60bbe3fcd72..d06665197db 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -9,6 +9,7 @@ describe Environment, models: true do it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) } it { is_expected.to delegate_method(:stop_action).to(:last_deployment) } + it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } @@ -187,4 +188,15 @@ describe Environment, models: true do it { is_expected.to be false } end end + + describe '#actions_for' do + let(:deployment) { create(:deployment, environment: environment) } + let(:pipeline) { deployment.deployable.pipeline } + let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_BUILD_REF_NAME' )} + let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )} + + it 'returns a list of actions with matching environment' do + expect(environment.actions_for('review/master')).to contain_exactly(review_action) + end + end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index a611c5e3823..a09d8689ff2 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -17,6 +17,10 @@ describe Ci::API::API do let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' } + before do + stub_container_registry_config(enabled: false) + end + shared_examples 'no builds available' do context 'when runner sends version in User-Agent' do context 'for stable version' do @@ -53,6 +57,41 @@ describe Ci::API::API do it 'updates runner info' do expect { register_builds }.to change { runner.reload.contacted_at } end + + context 'registry credentials' do + let(:registry_credentials) do + { 'type' => 'registry', + 'url' => 'registry.example.com:5005', + 'username' => 'gitlab-ci-token', + 'password' => build.token } + end + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true, host_port: 'registry.example.com:5005') + end + + it 'sends registry credentials key' do + register_builds info: { platform: :darwin } + + expect(json_response).to have_key('credentials') + expect(json_response['credentials']).to include(registry_credentials) + end + end + + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false, host_port: 'registry.example.com:5005') + end + + it 'does not send registry credentials' do + register_builds info: { platform: :darwin } + + expect(json_response).to have_key('credentials') + expect(json_response['credentials']).not_to include(registry_credentials) + end + end + end end context 'when builds are finished' do diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 7aba4f08088..f15c45cbaac 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -261,7 +261,7 @@ describe "Authentication", "routing" do end describe "Groups", "routing" do - let(:name) { 'complex.group-name' } + let(:name) { 'complex.group-namegit' } it "to #show" do expect(get("/groups/#{name}")).to route_to('groups#show', id: name) diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 0220f7e1db2..e515bc9f89c 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -227,16 +227,6 @@ describe MergeRequests::RefreshService, services: true do end end - context 'when the source branch is deleted' do - it 'does not create a MergeRequestDiff record' do - refresh_service = service.new(@project, @user) - - expect do - refresh_service.execute(@oldrev, Gitlab::Git::BLANK_SHA, 'refs/heads/master') - end.not_to change { MergeRequestDiff.count } - end - end - def reload_mrs @merge_request.reload @fork_merge_request.reload |
