diff options
61 files changed, 641 insertions, 258 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 315cd1e598c..7c08c29d8a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -430,7 +430,7 @@ merge request: 1. [Newlines styleguide][newlines-styleguide] 1. [Testing](doc/development/testing.md) 1. [JavaScript (ES6)](https://github.com/airbnb/javascript) -1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/master/es5) +1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/es5-deprecated/es5) 1. [SCSS styleguide][scss-styleguide] 1. [Shell commands](doc/development/shell_commands.md) created by GitLab contributors to enhance security diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index abd410582de..0d91a54c7d4 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.2.4 +0.3.0 diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 index 11a3449d99a..f1b41911b73 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -26,7 +26,7 @@ class PipelinesStore { */ startTimeAgoLoops() { const startTimeLoops = () => { - this.timeLoopInterval = setInterval(function timeloopInterval() { + this.timeLoopInterval = setInterval(() => { this.$children[0].$children.reduce((acc, component) => { const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; acc.push(timeAgoComponent); diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 index 4becbc32681..919fcd0a07b 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -28,7 +28,7 @@ * All dropdown events are fired at the .dropdown-menu's parent element. */ bindEvents() { - $(this.container).on('shown.bs.dropdown', this.getBuildsList); + $(document).on('shown.bs.dropdown', this.container, this.getBuildsList); } /** diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6 index 95564152cce..30f6680a673 100644 --- a/app/assets/javascripts/vue_realtime_listener/index.js.es6 +++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6 @@ -14,5 +14,16 @@ window.addEventListener('focus', startIntervals); window.addEventListener('blur', removeIntervals); document.addEventListener('beforeunload', removeAll); + + // add removeAll methods to stack + const stack = gl.VueRealtimeListener.reset; + gl.VueRealtimeListener.reset = () => { + gl.VueRealtimeListener.reset = stack; + removeAll(); + stack(); + }; }; + + // remove all event listeners and intervals + gl.VueRealtimeListener.reset = () => undefined; // noop })(window.gl || (window.gl = {})); diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 578f2b8f038..cea3d088e94 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -49,7 +49,7 @@ class Admin::GroupsController < Admin::ApplicationController end def destroy - DestroyGroupService.new(@group, current_user).async_execute + Groups::DestroyService.new(@group, current_user).async_execute redirect_to admin_groups_path, alert: "Group '#{@group.name}' was scheduled for deletion." end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 264b14713fb..9b6c3dd33b8 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController end def destroy - DestroyGroupService.new(@group, current_user).async_execute + Groups::DestroyService.new(@group, current_user).async_execute redirect_to root_path, alert: "Group '#{@group.name}' was scheduled for deletion." end diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index 440259b643c..8a5a645ed0e 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -48,6 +48,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController objects.each do |object| if existing_oids.include?(object[:oid]) object[:actions] = download_actions(object) + + if Guest.can?(:download_code, project) + object[:authenticated] = true + end else object[:error] = { code: 404, diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index c5d93ce25bc..b033f7b5ea9 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -51,7 +51,7 @@ class Projects::NotesController < Projects::ApplicationController def destroy if note.editable? - Notes::DeleteService.new(project, current_user).execute(note) + Notes::DestroyService.new(project, current_user).execute(note) end respond_to do |format| diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 68bf01ba08d..b44f38d4a0c 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -24,7 +24,7 @@ class RegistrationsController < Devise::RegistrationsController end def destroy - DeleteUserService.new(current_user).execute(current_user) + Users::DestroyService.new(current_user).execute(current_user) respond_to do |format| format.html do diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 91b24b8bc29..b5f8c23a667 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -64,11 +64,11 @@ module MergeRequestsHelper end def mr_closes_issues - @mr_closes_issues ||= @merge_request.closes_issues + @mr_closes_issues ||= @merge_request.closes_issues(current_user) end def mr_issues_mentioned_but_not_closing - @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing + @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing(current_user) end def mr_change_branches_path(merge_request) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index c568cca9e5e..d7d51c99979 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -86,7 +86,9 @@ module TodosHelper [ { id: '', text: 'Any Action' }, { id: Todo::ASSIGNED, text: 'Assigned' }, - { id: Todo::MENTIONED, text: 'Mentioned' } + { id: Todo::MENTIONED, text: 'Mentioned' }, + { id: Todo::MARKED, text: 'Added' }, + { id: Todo::BUILD_FAILED, text: 'Pipelines' } ] end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5213ea9d02b..8c1b076c2d7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -20,7 +20,7 @@ module Ci end serialize :options - serialize :yaml_variables, Gitlab::Serialize::Ci::Variables + serialize :yaml_variables, Gitlab::Serializer::Ci::Variables validates :coverage, numericality: true, allow_blank: true validates_presence_of :ref diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 43085f69105..c0d4dd0197f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -546,7 +546,7 @@ class MergeRequest < ActiveRecord::Base # Calculating this information for a number of merge requests requires # running `ReferenceExtractor` on each of them separately. # This optimization does not apply to issues from external sources. - def cache_merge_request_closes_issues!(current_user = self.author) + def cache_merge_request_closes_issues!(current_user) return if project.has_external_issue_tracker? transaction do @@ -558,10 +558,6 @@ class MergeRequest < ActiveRecord::Base end end - def closes_issue?(issue) - closes_issues.include?(issue) - end - # Return the set of issues that will be closed if this merge request is accepted. def closes_issues(current_user = self.author) if target_branch == project.default_branch @@ -575,13 +571,13 @@ class MergeRequest < ActiveRecord::Base end end - def issues_mentioned_but_not_closing(current_user = self.author) + def issues_mentioned_but_not_closing(current_user) return [] unless target_branch == project.default_branch ext = Gitlab::ReferenceExtractor.new(project, current_user) ext.analyze(description) - ext.issues - closes_issues + ext.issues - closes_issues(current_user) end def target_project_path diff --git a/app/models/namespace.rb b/app/models/namespace.rb index c2554317ad3..6de4d08fc28 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -7,6 +7,11 @@ class Namespace < ActiveRecord::Base include Gitlab::CurrentSettings include Routable + # Prevent users from creating unreasonably deep level of nesting. + # The number 20 was taken based on maximum nesting level of + # Android repo (15) + some extra backup. + NUMBER_OF_ANCESTORS_ALLOWED = 20 + cache_markdown_field :description, pipeline: :description has_many :projects, dependent: :destroy @@ -29,6 +34,8 @@ class Namespace < ActiveRecord::Base length: { maximum: 255 }, namespace: true + validate :nesting_level_allowed + delegate :name, to: :owner, allow_nil: true, prefix: true after_update :move_dir, if: :path_changed? @@ -177,7 +184,7 @@ class Namespace < ActiveRecord::Base # Scopes the model on ancestors of the record def ancestors if parent_id - path = route.path + path = route ? route.path : full_path paths = [] until path.blank? @@ -257,4 +264,10 @@ class Namespace < ActiveRecord::Base path_was end end + + def nesting_level_allowed + if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED + errors.add(:parent_id, "has too deep level of nesting") + end + end end diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index 91955542f25..fe16a3784c4 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -1,3 +1,50 @@ class EnvironmentSerializer < BaseSerializer + Item = Struct.new(:name, :size, :latest) + entity EnvironmentEntity + + def within_folders + tap { @itemize = true } + end + + def with_pagination(request, response) + tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) } + end + + def itemized? + @itemize + end + + def paginated? + @paginator.present? + end + + def represent(resource, opts = {}) + resource = @paginator.paginate(resource) if paginated? + + if itemized? + itemize(resource).map do |item| + { name: item.name, + size: item.size, + latest: super(item.latest, opts) } + end + else + super(resource, opts) + end + end + + private + + def itemize(resource) + items = resource.group(:item_name).order('item_name ASC') + .pluck('COALESCE(environment_type, name) AS item_name', + 'COUNT(*) AS environments_count', + 'MAX(id) AS last_environment_id') + + environments = resource.where(id: items.map(&:last)).index_by(&:id) + + items.map do |name, size, id| + Item.new(name, size, environments[id]) + end + end end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index b2de6c5832e..2bc6cf3266e 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -1,41 +1,25 @@ class PipelineSerializer < BaseSerializer class InvalidResourceError < StandardError; end - include API::Helpers::Pagination - Struct.new('Pagination', :request, :response) entity PipelineEntity - def represent(resource, opts = {}) - if paginated? - raise InvalidResourceError unless resource.respond_to?(:page) - - super(paginate(resource.includes(project: :namespace)), opts) - else - super(resource, opts) - end - end - - def paginated? - defined?(@pagination) - end - def with_pagination(request, response) - tap { @pagination = Struct::Pagination.new(request, response) } + tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) } end - private - - # Methods needed by `API::Helpers::Pagination` - # - def params - @pagination.request.query_parameters + def paginated? + @paginator.present? end - def request - @pagination.request - end + def represent(resource, opts = {}) + if resource.is_a?(ActiveRecord::Relation) + resource = resource.includes(project: :namespace) + end - def header(header, value) - @pagination.response.headers[header] = value + if paginated? + super(@paginator.paginate(resource), opts) + else + super(resource, opts) + end end end diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb deleted file mode 100644 index eaff88d6463..00000000000 --- a/app/services/delete_user_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -class DeleteUserService - attr_accessor :current_user - - def initialize(current_user) - @current_user = current_user - end - - def execute(user, options = {}) - if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present? - user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user' - return user - end - - user.solo_owned_groups.each do |group| - DestroyGroupService.new(group, current_user).execute - end - - user.personal_projects.each do |project| - # Skip repository removal because we remove directory with namespace - # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute - end - - # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing - namespace = user.namespace - user_data = user.destroy - namespace.really_destroy! - - user_data - end -end diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb deleted file mode 100644 index 2316c57bf1e..00000000000 --- a/app/services/destroy_group_service.rb +++ /dev/null @@ -1,29 +0,0 @@ -class DestroyGroupService - attr_accessor :group, :current_user - - def initialize(group, user) - @group, @current_user = group, user - end - - def async_execute - # Soft delete via paranoia gem - group.destroy - job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) - Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") - end - - def execute - group.projects.each do |project| - # Execute the destruction of the models immediately to ensure atomic cleanup. - # Skip repository removal because we remove directory with namespace - # that contain all these repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute - end - - group.children.each do |group| - DestroyGroupService.new(group, current_user).async_execute - end - - group.really_destroy! - end -end diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb new file mode 100644 index 00000000000..7f2d28086f5 --- /dev/null +++ b/app/services/groups/destroy_service.rb @@ -0,0 +1,25 @@ +module Groups + class DestroyService < Groups::BaseService + def async_execute + # Soft delete via paranoia gem + group.destroy + job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) + Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") + end + + def execute + group.projects.each do |project| + # Execute the destruction of the models immediately to ensure atomic cleanup. + # Skip repository removal because we remove directory with namespace + # that contain all these repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + end + + group.children.each do |group| + DestroyService.new(group, current_user).async_execute + end + + group.really_destroy! + end + end +end diff --git a/app/services/notes/delete_service.rb b/app/services/notes/destroy_service.rb index a673e8e9dde..b819bd17039 100644 --- a/app/services/notes/delete_service.rb +++ b/app/services/notes/destroy_service.rb @@ -1,5 +1,5 @@ module Notes - class DeleteService < BaseService + class DestroyService < BaseService def execute(note) note.destroy end diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb new file mode 100644 index 00000000000..2d11305be13 --- /dev/null +++ b/app/services/users/destroy_service.rb @@ -0,0 +1,33 @@ +module Users + class DestroyService + attr_accessor :current_user + + def initialize(current_user) + @current_user = current_user + end + + def execute(user, options = {}) + if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present? + user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user' + return user + end + + user.solo_owned_groups.each do |group| + Groups::DestroyService.new(group, current_user).execute + end + + user.personal_projects.each do |project| + # Skip repository removal because we remove directory with namespace + # that contain all this repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute + end + + # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing + namespace = user.namespace + user_data = user.destroy + namespace.really_destroy! + + user_data + end + end +end diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index d94f23f5a38..08cb8a04413 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -22,9 +22,7 @@ = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' - elsif create_mr_button?(@repository.root_ref, @ref) .control - = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do - = icon('plus') - Create Merge Request + = link_to "Create Merge Request", create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' .control = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index d76d48187cd..08236216421 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -23,6 +23,4 @@ - if @merge_request.present? = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn' - elsif create_mr_button? - = link_to create_mr_path, class: 'prepend-left-10 btn' do - = icon("plus") - Create Merge Request + = link_to "Create Merge Request", create_mr_path, class: 'prepend-left-10 btn' diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index c2f4457b60b..5d4e593e4ef 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,7 +1,7 @@ - content_for :note_actions do - if can?(current_user, :update_issue, @issue) - = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' #notes = render 'projects/notes/notes_with_form' diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index a2305f4f547..d3eb3b7055b 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -35,9 +35,9 @@ = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link' - if can?(current_user, :update_issue, @issue) %li - = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' %li - = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' %li = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) - if @issue.submittable_as_spam? && current_user.admin? @@ -48,8 +48,8 @@ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do New issue - if can?(current_user, :update_issue, @issue) - = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - if @issue.submittable_as_spam? && current_user.admin? = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb index 3194c389b3d..5483bbb210b 100644 --- a/app/workers/delete_user_worker.rb +++ b/app/workers/delete_user_worker.rb @@ -6,6 +6,6 @@ class DeleteUserWorker delete_user = User.find(delete_user_id) current_user = User.find(current_user_id) - DeleteUserService.new(current_user).execute(delete_user, options.symbolize_keys) + Users::DestroyService.new(current_user).execute(delete_user, options.symbolize_keys) end end diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb index a49a5fd0855..07e82767b06 100644 --- a/app/workers/group_destroy_worker.rb +++ b/app/workers/group_destroy_worker.rb @@ -11,6 +11,6 @@ class GroupDestroyWorker user = User.find(user_id) - DestroyGroupService.new(group, user).execute + Groups::DestroyService.new(group, user).execute end end diff --git a/changelogs/unreleased/20495-plus-icon-button.yml b/changelogs/unreleased/20495-plus-icon-button.yml new file mode 100644 index 00000000000..0f8650eb7b6 --- /dev/null +++ b/changelogs/unreleased/20495-plus-icon-button.yml @@ -0,0 +1,4 @@ +--- +title: Remove plus icon from MR button on compare view +merge_request: +author: diff --git a/changelogs/unreleased/23104-remove-public-param-for-projects.yml b/changelogs/unreleased/23104-remove-public-param-for-projects.yml new file mode 100644 index 00000000000..78eb785279f --- /dev/null +++ b/changelogs/unreleased/23104-remove-public-param-for-projects.yml @@ -0,0 +1,4 @@ +--- +title: 'API: remove `public` param for projects' +merge_request: 8736 +author: diff --git a/changelogs/unreleased/26705-filter-todos-by-manual-add.yml b/changelogs/unreleased/26705-filter-todos-by-manual-add.yml new file mode 100644 index 00000000000..3521496a20e --- /dev/null +++ b/changelogs/unreleased/26705-filter-todos-by-manual-add.yml @@ -0,0 +1,4 @@ +--- +title: Filter todos by manual add +merge_request: 8691 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/lfs-noauth-public-repo.yml b/changelogs/unreleased/lfs-noauth-public-repo.yml new file mode 100644 index 00000000000..60f62d7691b --- /dev/null +++ b/changelogs/unreleased/lfs-noauth-public-repo.yml @@ -0,0 +1,4 @@ +--- +title: Support unauthenticated LFS object downloads for public projects +merge_request: 8824 +author: Ben Boeckel diff --git a/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml b/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml new file mode 100644 index 00000000000..0751047c3c0 --- /dev/null +++ b/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml @@ -0,0 +1,4 @@ +--- +title: pass in current_user in MergeRequest and MergeRequestsHelper +merge_request: 8624 +author: Dongqing Hu diff --git a/changelogs/unreleased/removal_of_unused_parameter.yml b/changelogs/unreleased/removal_of_unused_parameter.yml new file mode 100644 index 00000000000..26bffafd9d9 --- /dev/null +++ b/changelogs/unreleased/removal_of_unused_parameter.yml @@ -0,0 +1,4 @@ +--- +title: 'removed unused parameter ''status_only: true''' +merge_request: +author: diff --git a/changelogs/unreleased/rename_delete_services.yml b/changelogs/unreleased/rename_delete_services.yml new file mode 100644 index 00000000000..686a1ef3d55 --- /dev/null +++ b/changelogs/unreleased/rename_delete_services.yml @@ -0,0 +1,4 @@ +--- +title: Fix inconsistent naming for services that delete things +merge_request: 5803 +author: dixpac diff --git a/doc/api/projects.md b/doc/api/projects.md index 122075bbd11..040153ac880 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -642,7 +642,6 @@ Parameters: | `snippets_enabled` | boolean | no | Enable snippets for this project | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | -| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 | | `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | @@ -676,7 +675,6 @@ Parameters: | `snippets_enabled` | boolean | no | Enable snippets for this project | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | -| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 | | `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | @@ -709,7 +707,6 @@ Parameters: | `snippets_enabled` | boolean | no | Enable snippets for this project | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | -| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 | | `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 73dde599b7e..dbb3c1113e8 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -113,7 +113,7 @@ DELETE /projects/:id/repository/files ``` ```bash -curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' +curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' ``` Example response: diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 008872b59a7..699318e2479 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -237,23 +237,24 @@ GFM will turn that reference into a link so you can navigate between them easily GFM will recognize the following: -| input | references | -|:-----------------------|:--------------------------- | -| `@user_name` | specific user | -| `@group_name` | specific group | -| `@all` | entire team | -| `#123` | issue | -| `!123` | merge request | -| `$123` | snippet | -| `~123` | label by ID | -| `~bug` | one-word label by name | -| `~"feature request"` | multi-word label by name | -| `%123` | milestone by ID | -| `%v1.23` | one-word milestone by name | -| `%"release candidate"` | multi-word milestone by name | -| `9ba12248` | specific commit | -| `9ba12248...b19a04f5` | commit range comparison | -| `[README](doc/README)` | repository file references | +| input | references | +|:---------------------------|:--------------------------------| +| `@user_name` | specific user | +| `@group_name` | specific group | +| `@all` | entire team | +| `#123` | issue | +| `!123` | merge request | +| `$123` | snippet | +| `~123` | label by ID | +| `~bug` | one-word label by name | +| `~"feature request"` | multi-word label by name | +| `%123` | milestone by ID | +| `%v1.23` | one-word milestone by name | +| `%"release candidate"` | multi-word milestone by name | +| `9ba12248` | specific commit | +| `9ba12248...b19a04f5` | commit range comparison | +| `[README](doc/README)` | repository file references | +| `[README](doc/README#L13)` | repository file line references | GFM also recognizes certain cross-project references: diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 7682d286866..50dadd94b04 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -125,7 +125,7 @@ module API delete ":id" do group = find_group!(params[:id]) authorize! :admin_group, group - DestroyGroupService.new(group, current_user).execute + ::Groups::DestroyService.new(group, current_user).execute end desc 'Get a list of projects in this group.' do diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 4d2a8f48267..8beccaaabd1 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -131,7 +131,7 @@ module API note = user_project.notes.find(params[:note_id]) authorize! :admin_note, note - ::Notes::DeleteService.new(user_project, current_user).execute(note) + ::Notes::DestroyService.new(user_project, current_user).execute(note) present note, with: Entities::Note end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 92a70faf1c2..bd4b23195ac 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -16,7 +16,6 @@ module API optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project' optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project' - optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.' optional :visibility_level, type: Integer, values: [ Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL, @@ -26,16 +25,6 @@ module API optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' end - - def map_public_to_visibility_level(attrs) - publik = attrs.delete(:public) - if !publik.nil? && !attrs[:visibility_level].present? - # Since setting the public attribute to private could mean either - # private or internal, use the more conservative option, private. - attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE - end - attrs - end end resource :projects do @@ -161,7 +150,7 @@ module API use :create_params end post do - attrs = map_public_to_visibility_level(declared_params(include_missing: false)) + attrs = declared_params(include_missing: false) project = ::Projects::CreateService.new(current_user, attrs).execute if project.saved? @@ -190,7 +179,7 @@ module API user = User.find_by(id: params.delete(:user_id)) not_found!('User') unless user - attrs = map_public_to_visibility_level(declared_params(include_missing: false)) + attrs = declared_params(include_missing: false) project = ::Projects::CreateService.new(user, attrs).execute if project.saved? @@ -268,14 +257,14 @@ module API at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :shared_runners_enabled, :container_registry_enabled, - :lfs_enabled, :public, :visibility_level, :public_builds, + :lfs_enabled, :visibility_level, :public_builds, :request_access_enabled, :only_allow_merge_if_build_succeeds, :only_allow_merge_if_all_discussions_are_resolved, :path, :default_branch end put ':id' do authorize_admin_project - attrs = map_public_to_visibility_level(declared_params(include_missing: false)) + attrs = declared_params(include_missing: false) authorize! :rename_project, user_project if attrs[:name].present? authorize! :change_visibility_level, user_project if attrs[:visibility_level].present? diff --git a/lib/api/users.rb b/lib/api/users.rb index 0ed468626b7..4980a90f952 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -293,7 +293,7 @@ module API user = User.find_by(id: params[:id]) not_found!('User') unless user - DeleteUserService.new(current_user).execute(user) + ::Users::DestroyService.new(current_user).execute(user) end desc 'Block a user. Available only for admins.' diff --git a/lib/gitlab/serialize/ci/variables.rb b/lib/gitlab/serializer/ci/variables.rb index 3a9443bfcd9..c059c454eac 100644 --- a/lib/gitlab/serialize/ci/variables.rb +++ b/lib/gitlab/serializer/ci/variables.rb @@ -1,5 +1,5 @@ module Gitlab - module Serialize + module Serializer module Ci # This serializer could make sure our YAML variables' keys and values # are always strings. This is more for legacy build data because diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb new file mode 100644 index 00000000000..bf2c0acc729 --- /dev/null +++ b/lib/gitlab/serializer/pagination.rb @@ -0,0 +1,36 @@ +module Gitlab + module Serializer + class Pagination + class InvalidResourceError < StandardError; end + include ::API::Helpers::Pagination + + def initialize(request, response) + @request = request + @response = response + end + + def paginate(resource) + if resource.respond_to?(:page) + super(resource) + else + raise InvalidResourceError + end + end + + private + + # Methods needed by `API::Helpers::Pagination` + # + + attr_reader :request + + def params + @request.query_parameters + end + + def header(header, value) + @response.headers[header] = value + end + end + end +end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 91d6f39a5bf..275561502cd 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -24,6 +24,10 @@ FactoryGirl.define do target factory: :merge_request end + trait :marked do + action { Todo::MARKED } + end + trait :approval_required do action { Todo::APPROVAL_REQUIRED } end diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index fb3a1ae4bd0..957e913bf95 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -29,8 +29,6 @@ describe 'Merge request', :feature, :js do it 'shows widget status after creating new merge request' do click_button 'Submit merge request' - expect(find('.mr-state-widget')).to have_content('Checking ability to merge automatically') - wait_for_ajax expect(page).to have_selector('.accept_merge_request') diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb index d1f2bc78884..e8f06916d53 100644 --- a/spec/features/todos/todos_filtering_spec.rb +++ b/spec/features/todos/todos_filtering_spec.rb @@ -98,15 +98,58 @@ describe 'Dashboard > User filters todos', feature: true, js: true do expect(find('.todos-list')).not_to have_content merge_request.to_reference end - it 'filters by action' do - click_button 'Action' - within '.dropdown-menu-action' do - click_link 'Assigned' + describe 'filter by action' do + before do + create(:todo, :build_failed, user: user_1, author: user_2, project: project_1) + create(:todo, :marked, user: user_1, author: user_2, project: project_1, target: issue) end - wait_for_ajax + it 'filters by Assigned' do + filter_action('Assigned') + + expect_to_see_action(:assigned) + end + + it 'filters by Mentioned' do + filter_action('Mentioned') + + expect_to_see_action(:mentioned) + end + + it 'filters by Added' do + filter_action('Added') + + expect_to_see_action(:marked) + end + + it 'filters by Pipelines' do + filter_action('Pipelines') - expect(find('.todos-list')).to have_content ' assigned you ' - expect(find('.todos-list')).not_to have_content ' mentioned ' + expect_to_see_action(:build_failed) + end + + def filter_action(name) + click_button 'Action' + within '.dropdown-menu-action' do + click_link name + end + + wait_for_ajax + end + + def expect_to_see_action(action_name) + action_names = { + assigned: ' assigned you ', + mentioned: ' mentioned ', + marked: ' added a todo for ', + build_failed: ' build failed for ' + } + + action_name_text = action_names.delete(action_name) + expect(find('.todos-list')).to have_content action_name_text + action_names.each_value do |other_action_text| + expect(find('.todos-list')).not_to have_content other_action_text + end + end end end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 8d268dbcf36..25f23826648 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -79,4 +79,86 @@ describe MergeRequestsHelper do expect(mr_widget_refresh_url(nil)).to eq('') end end + + describe '#mr_closes_issues' do + let(:user_1) { create(:user) } + let(:user_2) { create(:user) } + + let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) } + let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) } + + let(:issue_1) { create(:issue, project: project_1) } + let(:issue_2) { create(:issue, project: project_2) } + + let(:merge_request) { create(:merge_request, source_project: project_1, target_project: project_1,) } + + let(:merge_request) do + create(:merge_request, + source_project: project_1, target_project: project_1, + description: "Fixes #{issue_1.to_reference} Fixes #{issue_2.to_reference(project_1)}") + end + + before do + project_1.team << [user_2, :developer] + project_2.team << [user_2, :developer] + allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch) + @merge_request = merge_request + end + + context 'user without access to another private project' do + let(:current_user) { user_1 } + + it 'cannot see that project\'s issue that will be closed on acceptance' do + expect(mr_closes_issues).to contain_exactly(issue_1) + end + end + + context 'user with access to another private project' do + let(:current_user) { user_2 } + + it 'can see that project\'s issue that will be closed on acceptance' do + expect(mr_closes_issues).to contain_exactly(issue_1, issue_2) + end + end + end + + describe '#mr_issues_mentioned_but_not_closing' do + let(:user_1) { create(:user) } + let(:user_2) { create(:user) } + + let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) } + let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) } + + let(:issue_1) { create(:issue, project: project_1) } + let(:issue_2) { create(:issue, project: project_2) } + + let(:merge_request) do + create(:merge_request, + source_project: project_1, target_project: project_1, + description: "#{issue_1.to_reference} #{issue_2.to_reference(project_1)}") + end + + before do + project_1.team << [user_2, :developer] + project_2.team << [user_2, :developer] + allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch) + @merge_request = merge_request + end + + context 'user without access to another private project' do + let(:current_user) { user_1 } + + it 'cannot see that project\'s issue that will be closed on acceptance' do + expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1) + end + end + + context 'user with access to another private project' do + let(:current_user) { user_2 } + + it 'can see that project\'s issue that will be closed on acceptance' do + expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1, issue_2) + end + end + end end diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 index 559723bafcc..789f5dc9f49 100644 --- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 +++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 @@ -7,6 +7,9 @@ describe('Store', () => { store = new gl.commits.pipelines.PipelinesStore(); }); + // unregister intervals and event handlers + afterEach(() => gl.VueRealtimeListener.reset()); + it('should start with a blank state', () => { expect(store.state.pipelines.length).toBe(0); }); diff --git a/spec/lib/gitlab/serialize/ci/variables_spec.rb b/spec/lib/gitlab/serializer/ci/variables_spec.rb index 7ea74da5252..b810c68ea03 100644 --- a/spec/lib/gitlab/serialize/ci/variables_spec.rb +++ b/spec/lib/gitlab/serializer/ci/variables_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Serialize::Ci::Variables do +describe Gitlab::Serializer::Ci::Variables do subject do described_class.load(described_class.dump(object)) end diff --git a/spec/lib/gitlab/serializer/pagination_spec.rb b/spec/lib/gitlab/serializer/pagination_spec.rb new file mode 100644 index 00000000000..519eb1b274f --- /dev/null +++ b/spec/lib/gitlab/serializer/pagination_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Gitlab::Serializer::Pagination do + let(:request) { spy('request') } + let(:response) { spy('response') } + let(:headers) { spy('headers') } + + before do + allow(request).to receive(:query_parameters) + .and_return(params) + + allow(response).to receive(:headers) + .and_return(headers) + end + + let(:pagination) { described_class.new(request, response) } + + describe '#paginate' do + subject { pagination.paginate(resource) } + + let(:resource) { User.all } + let(:params) { { page: 1, per_page: 2 } } + + context 'when a multiple resources are present in relation' do + before { create_list(:user, 3) } + + it 'correctly paginates the resource' do + expect(subject.count).to be 2 + end + + it 'appends relevant headers' do + expect(headers).to receive(:[]=).with('X-Total', '3') + expect(headers).to receive(:[]=).with('X-Total-Pages', '2') + expect(headers).to receive(:[]=).with('X-Per-Page', '2') + + subject + end + end + + context 'when an invalid resource is about to be paginated' do + let(:resource) { create(:user) } + + it 'raises error' do + expect { subject }.to raise_error( + described_class::InvalidResourceError) + end + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index e1e99300489..a01741a9971 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -97,7 +97,7 @@ describe MergeRequest, models: true do commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") allow(subject).to receive(:commits).and_return([commit]) - expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1) + expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1) end it 'does not cache issues from external trackers' do @@ -106,7 +106,7 @@ describe MergeRequest, models: true do commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") allow(subject).to receive(:commits).and_return([commit]) - expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count) + expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count) end end @@ -300,7 +300,7 @@ describe MergeRequest, models: true do allow(subject.project).to receive(:default_branch). and_return(subject.target_branch) - expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue]) + expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue]) end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 5d2828b9bb0..35d932f1c64 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -3,21 +3,32 @@ require 'spec_helper' describe Namespace, models: true do let!(:namespace) { create(:namespace) } - it { is_expected.to have_many :projects } - it { is_expected.to have_many :project_statistics } - it { is_expected.to belong_to :parent } - it { is_expected.to have_many :children } + describe 'associations' do + it { is_expected.to have_many :projects } + it { is_expected.to have_many :project_statistics } + it { is_expected.to belong_to :parent } + it { is_expected.to have_many :children } + end - it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } - it { is_expected.to validate_length_of(:name).is_at_most(255) } + describe 'validations' do + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } + it { is_expected.to validate_length_of(:name).is_at_most(255) } + it { is_expected.to validate_length_of(:description).is_at_most(255) } + it { is_expected.to validate_presence_of(:path) } + it { is_expected.to validate_length_of(:path).is_at_most(255) } + it { is_expected.to validate_presence_of(:owner) } - it { is_expected.to validate_length_of(:description).is_at_most(255) } + it 'does not allow too deep nesting' do + ancestors = (1..21).to_a + nested = build(:namespace, parent: namespace) - it { is_expected.to validate_presence_of(:path) } - it { is_expected.to validate_length_of(:path).is_at_most(255) } + allow(nested).to receive(:ancestors).and_return(ancestors) - it { is_expected.to validate_presence_of(:owner) } + expect(nested).not_to be_valid + expect(nested.errors[:parent_id].first).to eq('has too deep level of nesting') + end + end describe "Respond to" do it { is_expected.to respond_to(:human_name) } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 225e2e005df..ac0bbec44e0 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -359,13 +359,6 @@ describe API::Projects, api: true do expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end - it 'sets a project as public using :public' do - project = attributes_for(:project, { public: true }) - post api('/projects', user), project - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - it 'sets a project as internal' do project = attributes_for(:project, :internal) post api('/projects', user), project @@ -373,13 +366,6 @@ describe API::Projects, api: true do expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end - it 'sets a project as internal overriding :public' do - project = attributes_for(:project, :internal, { public: true }) - post api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - it 'sets a project as private' do project = attributes_for(:project, :private) post api('/projects', user), project @@ -387,13 +373,6 @@ describe API::Projects, api: true do expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end - it 'sets a project as private using :public' do - project = attributes_for(:project, { public: false }) - post api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - it 'sets a project as allowing merge even if build fails' do project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) post api('/projects', user), project @@ -431,13 +410,14 @@ describe API::Projects, api: true do end context 'when a visibility level is restricted' do + let(:project_param) { attributes_for(:project, :public) } + before do - @project = attributes_for(:project, { public: true }) stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) end it 'does not allow a non-admin to use a restricted visibility level' do - post api('/projects', user), @project + post api('/projects', user), project_param expect(response).to have_http_status(400) expect(json_response['message']['visibility_level'].first).to( @@ -446,7 +426,8 @@ describe API::Projects, api: true do end it 'allows an admin to override restricted visibility settings' do - post api('/projects', admin), @project + post api('/projects', admin), project_param + expect(json_response['public']).to be_truthy expect(json_response['visibility_level']).to( eq(Gitlab::VisibilityLevel::PUBLIC) @@ -499,15 +480,6 @@ describe API::Projects, api: true do expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end - it 'sets a project as public using :public' do - project = attributes_for(:project, { public: true }) - post api("/projects/user/#{user.id}", admin), project - - expect(response).to have_http_status(201) - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - it 'sets a project as internal' do project = attributes_for(:project, :internal) post api("/projects/user/#{user.id}", admin), project @@ -517,14 +489,6 @@ describe API::Projects, api: true do expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end - it 'sets a project as internal overriding :public' do - project = attributes_for(:project, :internal, { public: true }) - post api("/projects/user/#{user.id}", admin), project - expect(response).to have_http_status(201) - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - it 'sets a project as private' do project = attributes_for(:project, :private) post api("/projects/user/#{user.id}", admin), project @@ -532,13 +496,6 @@ describe API::Projects, api: true do expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end - it 'sets a project as private using :public' do - project = attributes_for(:project, { public: false }) - post api("/projects/user/#{user.id}", admin), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - it 'sets a project as allowing merge even if build fails' do project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) post api("/projects/user/#{user.id}", admin), project @@ -865,7 +822,7 @@ describe API::Projects, api: true do it 'creates a new project snippet' do post api("/projects/#{project.id}/snippets", user), title: 'api test', file_name: 'sample.rb', code: 'test', - visibility_level: '0' + visibility_level: Gitlab::VisibilityLevel::PRIVATE expect(response).to have_http_status(201) expect(json_response['title']).to eq('api test') end @@ -1114,7 +1071,7 @@ describe API::Projects, api: true do end it 'updates visibility_level' do - project_param = { visibility_level: 20 } + project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } put api("/projects/#{project3.id}", user), project_param expect(response).to have_http_status(200) project_param.each_pair do |k, v| @@ -1124,7 +1081,7 @@ describe API::Projects, api: true do it 'updates visibility_level from public to private' do project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC }) - project_param = { public: false } + project_param = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } put api("/projects/#{project3.id}", user), project_param expect(response).to have_http_status(200) project_param.each_pair do |k, v| @@ -1197,7 +1154,7 @@ describe API::Projects, api: true do end it 'does not update visibility_level' do - project_param = { visibility_level: 20 } + project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } put api("/projects/#{project3.id}", user4), project_param expect(response).to have_http_status(403) end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 9bfc84c7425..c0e7bab8199 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -600,6 +600,7 @@ describe 'Git LFS API and storage' do expect(json_response).to eq('objects' => [ { 'oid' => sample_oid, 'size' => sample_size, + 'authenticated' => true, 'actions' => { 'download' => { 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 3c37660885d..1b95f1ff198 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -52,4 +52,136 @@ describe EnvironmentSerializer do expect(json).to be_an_instance_of Array end end + + context 'when representing environments within folders' do + let(:serializer) do + described_class.new(project: project).within_folders + end + + let(:resource) { Environment.all } + + subject { serializer.represent(resource) } + + context 'when there is a single environment' do + before { create(:environment, name: 'staging') } + + it 'represents one standalone environment' do + expect(subject.count).to eq 1 + expect(subject.first[:name]).to eq 'staging' + expect(subject.first[:size]).to eq 1 + expect(subject.first[:latest][:name]).to eq 'staging' + end + end + + context 'when there are multiple environments in folder' do + before do + create(:environment, name: 'staging/my-review-1') + create(:environment, name: 'staging/my-review-2') + end + + it 'represents one item that is a folder' do + expect(subject.count).to eq 1 + expect(subject.first[:name]).to eq 'staging' + expect(subject.first[:size]).to eq 2 + expect(subject.first[:latest][:name]).to eq 'staging/my-review-2' + expect(subject.first[:latest][:environment_type]).to eq 'staging' + end + end + + context 'when there are multiple folders and standalone environments' do + before do + create(:environment, name: 'staging/my-review-1') + create(:environment, name: 'staging/my-review-2') + create(:environment, name: 'production/my-review-3') + create(:environment, name: 'testing') + end + + it 'represents multiple items grouped within folders' do + expect(subject.count).to eq 3 + + expect(subject.first[:name]).to eq 'production' + expect(subject.first[:size]).to eq 1 + expect(subject.first[:latest][:name]).to eq 'production/my-review-3' + expect(subject.first[:latest][:environment_type]).to eq 'production' + expect(subject.second[:name]).to eq 'staging' + expect(subject.second[:size]).to eq 2 + expect(subject.second[:latest][:name]).to eq 'staging/my-review-2' + expect(subject.second[:latest][:environment_type]).to eq 'staging' + expect(subject.third[:name]).to eq 'testing' + expect(subject.third[:size]).to eq 1 + expect(subject.third[:latest][:name]).to eq 'testing' + expect(subject.third[:latest][:environment_type]).to be_nil + end + end + end + + context 'when used with pagination' do + let(:request) { spy('request') } + let(:response) { spy('response') } + let(:resource) { Environment.all } + let(:pagination) { { page: 1, per_page: 2 } } + + let(:serializer) do + described_class.new(project: project) + .with_pagination(request, response) + end + + before do + allow(request).to receive(:query_parameters) + .and_return(pagination) + end + + subject { serializer.represent(resource) } + + it 'creates a paginated serializer' do + expect(serializer).to be_paginated + end + + context 'when resource is paginatable relation' do + context 'when there is a single environment object in relation' do + before { create(:environment) } + + it 'serializes environments' do + expect(subject.first).to have_key :id + end + end + + context 'when multiple environment objects are serialized' do + before { create_list(:environment, 3) } + + it 'serializes appropriate number of objects' do + expect(subject.count).to be 2 + end + + it 'appends relevant headers' do + expect(response).to receive(:[]=).with('X-Total', '3') + expect(response).to receive(:[]=).with('X-Total-Pages', '2') + expect(response).to receive(:[]=).with('X-Per-Page', '2') + + subject + end + end + + context 'when grouping environments within folders' do + let(:serializer) do + described_class.new(project: project) + .with_pagination(request, response) + .within_folders + end + + before do + create(:environment, name: 'staging/review-1') + create(:environment, name: 'staging/review-2') + create(:environment, name: 'production/deploy-3') + create(:environment, name: 'testing') + end + + it 'paginates grouped items including ordering' do + expect(subject.count).to eq 2 + expect(subject.first[:name]).to eq 'production' + expect(subject.second[:name]).to eq 'staging' + end + end + end + end end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 7cbf131e41e..2aaef03cb93 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -52,14 +52,14 @@ describe PipelineSerializer do expect(serializer).to be_paginated end - context 'when resource does is not paginatable' do + context 'when resource is not paginatable' do context 'when a single pipeline object is being serialized' do let(:resource) { create(:ci_empty_pipeline) } let(:pagination) { { page: 1, per_page: 1 } } it 'raises error' do - expect { subject } - .to raise_error(PipelineSerializer::InvalidResourceError) + expect { subject }.to raise_error( + Gitlab::Serializer::Pagination::InvalidResourceError) end end end diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index 538e85cdc89..f86189b68e9 100644 --- a/spec/services/destroy_group_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' -describe DestroyGroupService, services: true do +describe Groups::DestroyService, services: true do include DatabaseConnectionHelpers - let!(:user) { create(:user) } - let!(:group) { create(:group) } - let!(:project) { create(:project, namespace: group) } + let!(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:project) { create(:project, namespace: group) } let!(:gitlab_shell) { Gitlab::Shell.new } - let!(:remove_path) { group.path + "+#{group.id}+deleted" } + let!(:remove_path) { group.path + "+#{group.id}+deleted" } shared_examples 'group destruction' do |async| context 'database records' do @@ -43,9 +43,9 @@ describe DestroyGroupService, services: true do def destroy_group(group, user, async) if async - DestroyGroupService.new(group, user).async_execute + Groups::DestroyService.new(group, user).async_execute else - DestroyGroupService.new(group, user).execute + Groups::DestroyService.new(group, user).execute end end end @@ -80,7 +80,7 @@ describe DestroyGroupService, services: true do # Kick off the initial group destroy in a new thread, so that # it doesn't share this spec's database transaction. - Thread.new { DestroyGroupService.new(group, user).async_execute }.join(5) + Thread.new { Groups::DestroyService.new(group, user).async_execute }.join(5) group_record = run_with_new_database_connection do |conn| conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first diff --git a/spec/services/notes/delete_service_spec.rb b/spec/services/notes/destroy_service_spec.rb index 1d0a747a480..f53f96e0c2b 100644 --- a/spec/services/notes/delete_service_spec.rb +++ b/spec/services/notes/destroy_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Notes::DeleteService, services: true do +describe Notes::DestroyService, services: true do describe '#execute' do it 'deletes a note' do project = create(:empty_project) diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/users/destroy_spec.rb index 418a12a83a9..46e58393218 100644 --- a/spec/services/delete_user_service_spec.rb +++ b/spec/services/users/destroy_spec.rb @@ -1,15 +1,16 @@ require 'spec_helper' -describe DeleteUserService, services: true do +describe Users::DestroyService, services: true do describe "Deletes a user and all their personal projects" do let!(:user) { create(:user) } let!(:current_user) { create(:user) } let!(:namespace) { create(:namespace, owner: user) } let!(:project) { create(:project, namespace: namespace) } + let(:service) { described_class.new(current_user) } context 'no options are given' do it 'deletes the user' do - user_data = DeleteUserService.new(current_user).execute(user) + user_data = service.execute(user) expect { user_data['email'].to eq(user.email) } expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) @@ -19,7 +20,7 @@ describe DeleteUserService, services: true do it 'will delete the project in the near future' do expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once - DeleteUserService.new(current_user).execute(user) + service.execute(user) end end @@ -30,7 +31,7 @@ describe DeleteUserService, services: true do before do solo_owned.group_members = [member] - DeleteUserService.new(current_user).execute(user) + service.execute(user) end it 'does not delete the user' do @@ -45,7 +46,7 @@ describe DeleteUserService, services: true do before do solo_owned.group_members = [member] - DeleteUserService.new(current_user).execute(user, delete_solo_owned_groups: true) + service.execute(user, delete_solo_owned_groups: true) end it 'deletes solo owned groups' do diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb index 14c56521280..0765573408c 100644 --- a/spec/workers/delete_user_worker_spec.rb +++ b/spec/workers/delete_user_worker_spec.rb @@ -5,14 +5,14 @@ describe DeleteUserWorker do let!(:current_user) { create(:user) } it "calls the DeleteUserWorker with the params it was given" do - expect_any_instance_of(DeleteUserService).to receive(:execute). + expect_any_instance_of(Users::DestroyService).to receive(:execute). with(user, {}) DeleteUserWorker.new.perform(current_user.id, user.id) end it "uses symbolized keys" do - expect_any_instance_of(DeleteUserService).to receive(:execute). + expect_any_instance_of(Users::DestroyService).to receive(:execute). with(user, test: "test") DeleteUserWorker.new.perform(current_user.id, user.id, "test" => "test") |