diff options
88 files changed, 1730 insertions, 286 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index c510a6da2d7..2a81c81b0f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -718,7 +718,7 @@ GEM redis-store (>= 1.3, < 2) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) - redis-rack (2.0.3) + redis-rack (2.0.4) rack (>= 1.5, < 3) redis-store (>= 1.2, < 2) redis-rails (5.0.2) diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 9280b7f150c..cb6e06ea584 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -64,3 +64,12 @@ export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - export function capitalizeFirstCharacter(text) { return `${text[0].toUpperCase()}${text.slice(1)}`; } + +/** + * Replaces all html tags from a string with the given replacement. + * + * @param {String} string + * @param {*} replace + * @returns {String} + */ +export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue new file mode 100644 index 00000000000..09276ba2769 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -0,0 +1,133 @@ +<script> + import simplePoll from '../../../lib/utils/simple_poll'; + import eventHub from '../../event_hub'; + import statusIcon from '../mr_widget_status_icon'; + import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; + import Flash from '../../../flash'; + + export default { + name: 'MRWidgetRebase', + props: { + mr: { + type: Object, + required: true, + }, + service: { + type: Object, + required: true, + }, + }, + components: { + statusIcon, + loadingIcon, + }, + data() { + return { + isMakingRequest: false, + rebasingError: null, + }; + }, + computed: { + status() { + if (this.mr.rebaseInProgress || this.isMakingRequest) { + return 'loading'; + } + if (!this.mr.canPushToSourceBranch && !this.mr.rebaseInProgress) { + return 'warning'; + } + return 'success'; + }, + showDisabledButton() { + return ['failed', 'loading'].includes(this.status); + }, + }, + methods: { + rebase() { + this.isMakingRequest = true; + this.rebasingError = null; + + this.service.rebase() + .then(() => { + simplePoll(this.checkRebaseStatus); + }) + .catch((error) => { + this.rebasingError = error.merge_error; + this.isMakingRequest = false; + Flash('Something went wrong. Please try again.'); + }); + }, + checkRebaseStatus(continuePolling, stopPolling) { + this.service.poll() + .then(res => res.data) + .then((res) => { + if (res.rebase_in_progress) { + continuePolling(); + } else { + this.isMakingRequest = false; + + if (res.merge_error && res.merge_error.length) { + this.rebasingError = res.merge_error; + Flash('Something went wrong. Please try again.'); + } + + eventHub.$emit('MRWidgetUpdateRequested'); + stopPolling(); + } + }) + .catch(() => { + this.isMakingRequest = false; + Flash('Something went wrong. Please try again.'); + stopPolling(); + }); + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + :status="status" + :show-disabled-button="showDisabledButton" + /> + + <div class="rebase-state-find-class-convention media media-body space-children"> + <template v-if="mr.rebaseInProgress || isMakingRequest"> + <span class="bold"> + Rebase in progress + </span> + </template> + <template v-if="!mr.rebaseInProgress && !mr.canPushToSourceBranch"> + <span class="bold"> + Fast-forward merge is not possible. + Rebase the source branch onto + <span class="label-branch">{{mr.targetBranch}}</span> + to allow this merge request to be merged. + </span> + </template> + <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> + <div class="accept-merge-holder clearfix js-toggle-container accept-action media space-children"> + <button + type="button" + class="btn btn-sm btn-reopen btn-success" + :disabled="isMakingRequest" + @click="rebase"> + <loading-icon v-if="isMakingRequest" /> + Rebase + </button> + <span + v-if="!rebasingError" + class="bold"> + Fast-forward merge is not possible. + Rebase the source branch onto the target branch or merge target + branch into source branch to allow this merge request to be merged. + </span> + <span + v-else + class="bold danger"> + {{rebasingError}} + </span> + </div> + </template> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index 5bd8b99420a..940f3d9b2d0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -32,6 +32,7 @@ export { default as UnresolvedDiscussionsState } from './components/states/mr_wi export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked'; export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds'; +export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed'; export { default as CheckingState } from './components/states/mr_widget_checking'; export { default as MRWidgetStore } from './stores/mr_widget_store'; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index fdae06200de..2075f8e4fec 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -10,6 +10,7 @@ import { MergedState, ClosedState, MergingState, + RebaseState, WipState, ArchivedState, ConflictsState, @@ -79,6 +80,7 @@ export default { ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath, statusPath: store.statusPath, mergeActionsContentPath: store.mergeActionsContentPath, + rebasePath: store.rebasePath, }; return new MRWidgetService(endpoints); }, @@ -232,6 +234,7 @@ export default { 'mr-widget-pipeline-failed': PipelineFailedState, 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, 'mr-widget-auto-merge-failed': AutoMergeFailed, + 'mr-widget-rebase': RebaseState, }, template: ` <div class="mr-state-widget prepend-top-default"> diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index 7c0bbdd403f..fecbfec2214 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -37,6 +37,10 @@ export default class MRWidgetService { return axios.get(this.endpoints.mergeActionsContentPath); } + rebase() { + return axios.post(this.endpoints.rebasePath); + } + static stopEnvironment(url) { return axios.post(url); } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js index 2bace3311c8..f7f0c1b6cb7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js @@ -25,6 +25,8 @@ export default function deviseState(data) { return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds; } else if (!this.canMerge) { return stateKey.notAllowedToMerge; + } else if (this.shouldBeRebased) { + return stateKey.rebase; } else if (this.canBeMerged) { return stateKey.readyToMerge; } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 474c17ec133..ed004b3bb08 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -26,6 +26,7 @@ export default class MergeRequestStore { this.divergedCommitsCount = data.diverged_commits_count; this.pipeline = data.pipeline || {}; this.deployments = this.deployments || data.deployments || []; + this.initRebase(data); if (data.issues_links) { const links = data.issues_links; @@ -124,6 +125,13 @@ export default class MergeRequestStore { return this.state === stateKey.nothingToMerge; } + initRebase(data) { + this.canPushToSourceBranch = data.can_push_to_source_branch; + this.rebaseInProgress = data.rebase_in_progress; + this.approvalsLeft = !data.approved; + this.rebasePath = data.rebase_path; + } + static buildMetrics(metrics) { if (!metrics) { return {}; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js index de980c175fb..29d5bd4a1da 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js @@ -17,6 +17,7 @@ const stateToComponentMap = { failedToMerge: 'mr-widget-failed-to-merge', autoMergeFailed: 'mr-widget-auto-merge-failed', shaMismatch: 'mr-widget-sha-mismatch', + rebase: 'mr-widget-rebase', }; const statesToShowHelpWidget = [ @@ -29,6 +30,7 @@ const statesToShowHelpWidget = [ 'pipelineFailed', 'pipelineBlocked', 'autoMergeFailed', + 'rebase', ]; export const stateKey = { @@ -46,6 +48,7 @@ export const stateKey = { mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds', notAllowedToMerge: 'notAllowedToMerge', readyToMerge: 'readyToMerge', + rebase: 'rebase', }; export default { diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue new file mode 100644 index 00000000000..05e48ed297f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -0,0 +1,46 @@ +<script> + import { __ } from '~/locale'; + /** + * Port of detail_behavior expand button. + * + * @example + * <expand-button> + * <template slot="expanded"> + * Text goes here. + * </template> + * </expand-button> + */ + export default { + name: 'expandButton', + data() { + return { + isCollapsed: true, + }; + }, + computed: { + ariaLabel() { + return __('Click to expand text'); + }, + }, + methods: { + onClick() { + this.isCollapsed = !this.isCollapsed; + }, + }, + }; +</script> +<template> + <span> + <button + type="button" + v-show="isCollapsed" + class="text-expander btn-blank" + :aria-label="ariaLabel" + @click="onClick"> + ... + </button> + <span v-show="!isCollapsed"> + <slot name="expanded"></slot> + </span> + </span> +</template> diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 6b59c8461a3..2e8a738b6d9 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,6 +10,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :set_issuables_index, only: [:index] before_action :authenticate_user!, only: [:assign_related_issues] + before_action :check_user_can_push_to_source_branch!, only: [:rebase] def index @merge_requests = @issuables @@ -223,6 +224,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo render json: environments end + def rebase + RebaseWorker.perform_async(@merge_request.id, current_user.id) + + render nothing: true, status: 200 + end + protected alias_method :subscribable_resource, :merge_request @@ -322,4 +329,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo @finder_type = MergeRequestsFinder super end + + def check_user_can_push_to_source_branch! + return access_denied! unless @merge_request.source_branch_exists? + + access_check = ::Gitlab::UserAccess + .new(current_user, project: @merge_request.source_project) + .can_push_to_branch?(@merge_request.source_branch) + + access_denied! unless access_check + end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 6f609348402..6f229b08c0c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -353,7 +353,7 @@ class ProjectsController < Projects::ApplicationController end def repo_exists? - project.repository_exists? && !project.empty_repo? && project.repo + project.repository_exists? && !project.empty_repo? rescue Gitlab::Git::Repository::NoRepository project.repository.expire_exists_cache diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index ce432ddbfe6..6de9eb89468 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -1,4 +1,6 @@ class LabelsFinder < UnionFinder + include Gitlab::Utils::StrongMemoize + def initialize(current_user, params = {}) @current_user = current_user @params = params @@ -32,6 +34,8 @@ class LabelsFinder < UnionFinder label_ids << project.labels end end + elsif only_group_labels? + label_ids << Label.where(group_id: group.id) else label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(project_id: projects.select(:id)) @@ -51,6 +55,13 @@ class LabelsFinder < UnionFinder items.where(title: title) end + def group + strong_memoize(:group) do + group = Group.find(params[:group_id]) + authorized_to_read_labels?(group) && group + end + end + def group? params[:group_id].present? end @@ -63,6 +74,10 @@ class LabelsFinder < UnionFinder params[:project_ids].present? end + def only_group_labels? + params[:only_group_labels] + end + def title params[:title] || params[:name] end @@ -96,9 +111,9 @@ class LabelsFinder < UnionFinder @projects end - def authorized_to_read_labels?(project) + def authorized_to_read_labels?(label_parent) return true if skip_authorization - Ability.allowed?(current_user, :read_label, project) + Ability.allowed?(current_user, :read_label, label_parent) end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c39789b047d..ef58816937c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -156,6 +156,13 @@ class MergeRequest < ActiveRecord::Base '!' end + def rebase_in_progress? + # The source project can be deleted + return false unless source_project + + source_project.repository.rebase_in_progress?(id) + end + # Use this method whenever you need to make sure the head_pipeline is synced with the # branch head commit, for example checking if a merge request can be merged. # For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004 @@ -607,7 +614,7 @@ class MergeRequest < ActiveRecord::Base check_if_can_be_merged - can_be_merged? + can_be_merged? && !should_be_rebased? end def mergeable_state?(skip_ci_check: false) diff --git a/app/models/project.rb b/app/models/project.rb index 5d6c1b30587..4cb9d9fe637 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -987,10 +987,6 @@ class Project < ActiveRecord::Base false end - def repo - repository.rugged - end - def url_to_repo gitlab_shell.url_to_repo(full_path) end @@ -1433,7 +1429,7 @@ class Project < ActiveRecord::Base # We'd need to keep track of project full path otherwise directory tree # created with hashed storage enabled cannot be usefully imported using # the import rake task. - repo.config['gitlab.fullpath'] = gl_full_path + repository.rugged.config['gitlab.fullpath'] = gl_full_path rescue Gitlab::Git::Repository::NoRepository => e Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.") nil diff --git a/app/models/repository.rb b/app/models/repository.rb index b1fd981965c..4bedcbfb6a2 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1099,6 +1099,13 @@ class Repository @project.repository_storage_path end + def rebase(user, merge_request) + raw.rebase(user, merge_request.id, branch: merge_request.source_branch, + branch_sha: merge_request.source_branch_sha, + remote_repository: merge_request.target_project.repository.raw, + remote_branch: merge_request.target_branch) + end + private # TODO Generice finder, later split this on finders by Ref or Oid diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index d2d45e402b0..f0bcba588a2 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -28,12 +28,18 @@ class GroupPolicy < BasePolicy with_options scope: :subject, score: 0 condition(:request_access_enabled) { @subject.request_access_enabled } - rule { public_group } .enable :read_group + rule { public_group }.policy do + enable :read_group + enable :read_list + enable :read_label + end + rule { logged_in_viewable }.enable :read_group rule { guest }.policy do enable :read_group enable :upload_file + enable :read_label end rule { admin } .enable :read_group diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index ab4c87c0169..c6806b7cc26 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -76,6 +76,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end end + def rebase_path + if !rebase_in_progress? && should_be_rebased? && user_can_push_to_source_branch? + rebase_project_merge_request_path(project, merge_request) + end + end + def target_branch_tree_path if target_branch_exists? project_tree_path(project, target_branch) @@ -152,6 +158,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated user_can_collaborate_with_project? && can_be_cherry_picked? end + def can_push_to_source_branch? + source_branch_exists? && user_can_push_to_source_branch? + end + private def conflicts @@ -174,6 +184,14 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end.sort.to_sentence end + def user_can_push_to_source_branch? + return false unless source_branch_exists? + + ::Gitlab::UserAccess + .new(current_user, project: source_project) + .can_push_to_branch?(source_branch) + end + def user_can_collaborate_with_project? can?(current_user, :push_code, project) || (current_user && current_user.already_forked?(project)) diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb index d54a6516aed..e4aec977f01 100644 --- a/app/serializers/merge_request_basic_entity.rb +++ b/app/serializers/merge_request_basic_entity.rb @@ -4,4 +4,5 @@ class MergeRequestBasicEntity < IssuableSidebarEntity expose :merge_error expose :state expose :source_branch_exists?, as: :source_branch_exists + expose :rebase_in_progress?, as: :rebase_in_progress end diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index e905e6876c2..48cd2317f46 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -23,6 +23,16 @@ class MergeRequestWidgetEntity < IssuableEntity MergeRequestMetricsEntity.new(metrics).as_json end + expose :rebase_commit_sha + expose :rebase_in_progress?, as: :rebase_in_progress + + expose :can_push_to_source_branch do |merge_request| + presenter(merge_request).can_push_to_source_branch? + end + expose :rebase_path do |merge_request| + presenter(merge_request).rebase_path + end + # User entities expose :merge_user, using: UserEntity diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb new file mode 100644 index 00000000000..0d5a25fa28e --- /dev/null +++ b/app/services/merge_requests/rebase_service.rb @@ -0,0 +1,30 @@ +module MergeRequests + class RebaseService < MergeRequests::WorkingCopyBaseService + def execute(merge_request) + @merge_request = merge_request + + if rebase + success + else + error('Failed to rebase. Should be done manually') + end + end + + def rebase + if merge_request.rebase_in_progress? + log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true) + return false + end + + rebase_sha = repository.rebase(current_user, merge_request) + + merge_request.update_attributes(rebase_commit_sha: rebase_sha) + + true + rescue => e + log_error('Failed to rebase branch:') + log_error(e.message, save_message_on_model: true) + false + end + end +end diff --git a/app/services/merge_requests/working_copy_base_service.rb b/app/services/merge_requests/working_copy_base_service.rb new file mode 100644 index 00000000000..186e05bf966 --- /dev/null +++ b/app/services/merge_requests/working_copy_base_service.rb @@ -0,0 +1,24 @@ +module MergeRequests + class WorkingCopyBaseService < MergeRequests::BaseService + attr_reader :merge_request + + def source_project + @source_project ||= merge_request.source_project + end + + def target_project + @target_project ||= merge_request.target_project + end + + def log_error(message, save_message_on_model: false) + Gitlab::GitLogger.error("#{self.class.name} error (#{merge_request.to_reference(full: true)}): #{message}") + + merge_request.update(merge_error: message) if save_message_on_model + end + + # Don't try to print expensive instance variables. + def inspect + "#<#{self.class} #{merge_request.to_reference(full: true)}>" + end + end +end diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml index 9d357293a2f..8129c72feb2 100644 --- a/app/views/projects/_merge_request_fast_forward_settings.html.haml +++ b/app/views/projects/_merge_request_fast_forward_settings.html.haml @@ -10,4 +10,4 @@ No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. %br %span.descr - When fast-forward merge is not possible, the user must first rebase locally. + When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/views/projects/_merge_request_rebase_settings.html.haml b/app/views/projects/_merge_request_rebase_settings.html.haml index c52e09573a6..54e0b73d24c 100644 --- a/app/views/projects/_merge_request_rebase_settings.html.haml +++ b/app/views/projects/_merge_request_rebase_settings.html.haml @@ -10,4 +10,4 @@ This way you could make sure that if this merge request would build, after merging to target branch it would also build. %br %span.descr - When fast-forward merge is not possible, the user must first rebase locally. + When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 268b7028fd9..fafd9e5ef00 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -89,6 +89,7 @@ - project_service - propagate_service_template - reactive_caching +- rebase - repository_fork - repository_import - storage_migrator diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb new file mode 100644 index 00000000000..090987778a2 --- /dev/null +++ b/app/workers/rebase_worker.rb @@ -0,0 +1,12 @@ +class RebaseWorker + include ApplicationWorker + + def perform(merge_request_id, current_user_id) + current_user = User.find(current_user_id) + merge_request = MergeRequest.find(merge_request_id) + + MergeRequests::RebaseService + .new(merge_request.source_project, current_user) + .execute(merge_request) + end +end diff --git a/changelogs/unreleased/40301-rebase.yml b/changelogs/unreleased/40301-rebase.yml new file mode 100644 index 00000000000..1c0fc0cd8ae --- /dev/null +++ b/changelogs/unreleased/40301-rebase.yml @@ -0,0 +1,5 @@ +--- +title: Allow user to rebase merge requests. +merge_request: +author: +type: added diff --git a/changelogs/unreleased/ac-autodevopfix-kubectl-version.yml b/changelogs/unreleased/ac-autodevopfix-kubectl-version.yml new file mode 100644 index 00000000000..0ceeb7ccee1 --- /dev/null +++ b/changelogs/unreleased/ac-autodevopfix-kubectl-version.yml @@ -0,0 +1,5 @@ +--- +title: Force Auto DevOps kubectl version to 1.8.6 +merge_request: 16218 +author: +type: fixed diff --git a/changelogs/unreleased/api-domains-expose-project_id.yml b/changelogs/unreleased/api-domains-expose-project_id.yml new file mode 100644 index 00000000000..22617ffe9b5 --- /dev/null +++ b/changelogs/unreleased/api-domains-expose-project_id.yml @@ -0,0 +1,5 @@ +--- +title: Expose project_id on /api/v4/pages/domains +merge_request: 16200 +author: Luc Didry +type: changed diff --git a/changelogs/unreleased/issues-40986-get-participants-from-issues-mr-api.yml b/changelogs/unreleased/issues-40986-get-participants-from-issues-mr-api.yml new file mode 100644 index 00000000000..4cac87b0cdb --- /dev/null +++ b/changelogs/unreleased/issues-40986-get-participants-from-issues-mr-api.yml @@ -0,0 +1,5 @@ +--- +title: 'API: get participants from merge_requests & issues' +merge_request: 16187 +author: Brent Greeff +type: added diff --git a/changelogs/unreleased/update-redis-rack.yml b/changelogs/unreleased/update-redis-rack.yml new file mode 100644 index 00000000000..6e2e6e203b8 --- /dev/null +++ b/changelogs/unreleased/update-redis-rack.yml @@ -0,0 +1,5 @@ +--- +title: Update redis-rack to 2.0.4 +merge_request: +author: +type: other diff --git a/config/routes/project.rb b/config/routes/project.rb index c3ad53a387f..1354c4c5537 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -96,6 +96,7 @@ constraints(ProjectUrlConstrainer.new) do post :toggle_subscription post :remove_wip post :assign_related_issues + post :rebase scope constraints: { format: nil }, action: :show do get :commits, defaults: { tab: 'commits' } diff --git a/db/migrate/20171230123729_add_rebase_commit_sha_to_merge_requests.rb b/db/migrate/20171230123729_add_rebase_commit_sha_to_merge_requests.rb new file mode 100644 index 00000000000..2ce156fa92e --- /dev/null +++ b/db/migrate/20171230123729_add_rebase_commit_sha_to_merge_requests.rb @@ -0,0 +1,7 @@ +class AddRebaseCommitShaToMergeRequests < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :merge_requests, :rebase_commit_sha, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 778d66f16b0..740e80ccfd4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171229225929) do +ActiveRecord::Schema.define(version: 20171230123729) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1099,6 +1099,7 @@ ActiveRecord::Schema.define(version: 20171229225929) do t.string "merge_jid" t.boolean "discussion_locked" t.integer "latest_merge_request_diff_id" + t.string "rebase_commit_sha" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree diff --git a/doc/api/boards.md b/doc/api/boards.md index 69c47abc806..246de50323e 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -15,10 +15,10 @@ GET /projects/:id/boards | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/boards +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards ``` Example response: @@ -27,6 +27,19 @@ Example response: [ { "id" : 1, + "project": { + "id": 5, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site" + }, + "milestone": { + "id": 12 + "title": "10.0" + }, "lists" : [ { "id" : 1, @@ -60,6 +73,74 @@ Example response: ] ``` +## Single board + +Get a single board. + +``` +GET /projects/:id/boards/:board_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1 +``` + +Example response: + +```json + { + "id": 1, + "name:": "project issue board", + "project": { + "id": 5, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site" + }, + "milestone": { + "id": 12 + "title": "10.0" + }, + "lists" : [ + { + "id" : 1, + "label" : { + "name" : "Testing", + "color" : "#F0AD4E", + "description" : null + }, + "position" : 1 + }, + { + "id" : 2, + "label" : { + "name" : "Ready", + "color" : "#FF0000", + "description" : null + }, + "position" : 2 + }, + { + "id" : 3, + "label" : { + "name" : "Production", + "color" : "#FF5F00", + "description" : null + }, + "position" : 3 + } + ] + } +``` + ## List board lists Get a list of the board's lists. @@ -71,8 +152,8 @@ GET /projects/:id/boards/:board_id/lists | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists @@ -122,9 +203,9 @@ GET /projects/:id/boards/:board_id/lists/:list_id | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `list_id`| integer | yes | The ID of a board's list | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `list_id`| integer | yes | The ID of a board's list | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1 @@ -154,9 +235,9 @@ POST /projects/:id/boards/:board_id/lists | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `label_id` | integer | yes | The ID of a label | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `label_id` | integer | yes | The ID of a label | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5 @@ -186,10 +267,10 @@ PUT /projects/:id/boards/:board_id/lists/:list_id | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `list_id` | integer | yes | The ID of a board's list | -| `position` | integer | yes | The position of the list | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `list_id` | integer | yes | The ID of a board's list | +| `position` | integer | yes | The position of the list | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1?position=2 @@ -219,9 +300,9 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `list_id` | integer | yes | The ID of a board's list | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `list_id` | integer | yes | The ID of a board's list | ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1 diff --git a/doc/api/issues.md b/doc/api/issues.md index d2fefbe68aa..da89db17cd9 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -1124,6 +1124,45 @@ Example response: ``` +## Participants on issues + +``` +GET /projects/:id/issues/:issue_iid/participants +``` + +| Attribute | Type | Required | Description | +|-------------|---------|----------|--------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `issue_iid` | integer | yes | The internal ID of a project's issue | + +```bash +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/participants +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "John Doe1", + "username": "user1", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://localhost/user1" + }, + { + "id": 5, + "name": "John Doe5", + "username": "user5", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80&d=identicon", + "web_url": "http://localhost/user5" + } +] +``` + + ## Comments on issues Comments are done via the [notes](notes.md) resource. diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 4d3592e8f71..24afcef9a31 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -308,6 +308,41 @@ Parameters: } ``` +## Get single MR participants + +Get a list of merge request participants. + +``` +GET /projects/:id/merge_requests/:merge_request_iid/participants +``` + +Parameters: + +- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user +- `merge_request_iid` (required) - The internal ID of the merge request + + +```json +[ + { + "id": 1, + "name": "John Doe1", + "username": "user1", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://localhost/user1" + }, + { + "id": 2, + "name": "John Doe2", + "username": "user2", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/10fc7f102be8de7657fb4d80898bbfe3?s=80&d=identicon", + "web_url": "http://localhost/user2" + }, +] +``` + ## Get single MR commits Get a list of merge request commits. diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md index 50685f335f7..20275b902c6 100644 --- a/doc/api/pages_domains.md +++ b/doc/api/pages_domains.md @@ -21,6 +21,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a { "domain": "ssl.domain.example", "url": "https://ssl.domain.example", + "project_id": 1337, "certificate": { "expired": false, "expiration": "2020-04-12T14:32:00.000Z" diff --git a/doc/api/settings.md b/doc/api/settings.md index 0e4758cda2d..0b5b1f0c134 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -69,7 +69,7 @@ PUT /application/settings | `after_sign_up_text` | string | no | Text shown to the user after signing up | | `akismet_api_key` | string | no | API key for akismet spam protection | | `akismet_enabled` | boolean | no | Enable or disable akismet spam protection | -| `circuitbreaker_access_retries | integer | no | The number of attempts GitLab will make to access a storage. | +| `circuitbreaker_access_retries` | integer | no | The number of attempts GitLab will make to access a storage. | | `circuitbreaker_check_interval` | integer | no | Number of seconds in between storage checks. | | `circuitbreaker_failure_count_threshold` | integer | no | The number of failures of after which GitLab will completely prevent access to the storage. | | `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. | diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md new file mode 100644 index 00000000000..abe5b06e0f0 --- /dev/null +++ b/doc/development/testing_guide/end_to_end_tests.md @@ -0,0 +1,80 @@ +# End-to-End Testing + +## What is End-to-End testing? + +End-to-End testing is a strategy used to check whether your application works +as expected across entire software stack and architecture, including +integration of all microservices and components that are supposed to work +together. + +## How do we test GitLab? + +We use [Omnibus GitLab][omnibus-gitlab] to build GitLab packages and then we +test these packages using [GitLab QA][gitlab-qa] project, which is entirely +black-box, click-driven testing framework. + +### Testing nightly builds + +We run scheduled pipeline each night to test nightly builds created by Omnibus. +You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pipelines]. + +### Testing code in merge requests + +It is possible to run end-to-end tests (eventually being run within a +[GitLab QA pipeline][gitlab-qa-pipelines]) for a merge request by triggering +the `package-qa` manual action, that should be present in a merge request +widget. + +Mmanual action that starts end-to-end tests is also available in merge requests +in Omnibus GitLab project. + +Below you can read more about how to use it and how does it work. + +#### How does it work? + +Currently, we are using _multi-project pipeline_-like approach to run QA +pipelines. + +1. Developer triggers a manual action, that can be found in CE and EE merge +requests. This starts a chain of pipelines in multiple projects. + +1. The script being executed triggers a pipeline in GitLab Omnibus and waits +for the resulting status. We call this a _status attribution_. + +1. GitLab packages are being built in Omnibus pipeline. Packages are going to be +pushed to Container Registry. + +1. When packages are ready, and available in the registry, a final step in the +pipeline, that is now running in Omnibus, triggers a new pipeline in the GitLab +QA project. It also waits for a resulting status. + +1. GitLab QA pulls images from the registry, spins-up containers and runs tests +against a test environment that has been just orchestrated by the `gitlab-qa` +tool. + +1. The result of the GitLab QA pipeline is being propagated upstream, through +Omnibus, back to CE / EE merge request. + +#### How do I write tests? + +In order to write new tests, you first need to learn more about GitLab QA +architecture. See the [documentation about it][gitlab-qa-architecture] in +GitLab QA project. + +Once you decided where to put test environment orchestration scenarios and +instance specs, take a look at the [relevant documentation][instance-qa-readme] +and examples in [the `qa/` directory][instance-qa-examples]. + +## Where can I ask for help? + +You can ask question in the `#qa` channel on Slack (GitLab internal) or you can +find an issue you would like to work on in [the issue tracker][gitlab-qa-issues] +and start a new discussion there. + +[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab +[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa +[gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines +[gitlab-qa-architecture]: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/architecture.md +[gitlab-qa-issues]: https://gitlab.com/gitlab-org/gitlab-qa/issues +[instance-qa-readme]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/README.md +[instance-qa-examples]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 65386f231a0..74d09eb91ff 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -65,6 +65,13 @@ Everything you should know about how to test Rake tasks. --- +## [End-to-end tests](end_to_end_tests.md) + +Everything you should know about how to run end-to-end tests using +[GitLab QA][gitlab-qa] testing framework. + +--- + ## Spinach (feature) tests GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) @@ -89,3 +96,4 @@ test should be re-implemented using RSpec instead. [Capybara]: https://github.com/teamcapybara/capybara [Karma]: http://karma-runner.github.io/ [Jasmine]: https://jasmine.github.io/ +[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index 1cbd4350284..4adf0dc7c7a 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -121,6 +121,9 @@ running feature tests (i.e. using Capybara) against it. The actual test scenarios and steps are [part of GitLab Rails] so that they're always in-sync with the codebase. +Read a separate document about [end-to-end tests](end_to_end_tests.md) to +learn more. + [multiple pieces]: ../architecture.md#components [GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell [GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse diff --git a/doc/user/project/merge_requests/fast_forward_merge.md b/doc/user/project/merge_requests/fast_forward_merge.md index 085170d9f03..3cd91a185e3 100644 --- a/doc/user/project/merge_requests/fast_forward_merge.md +++ b/doc/user/project/merge_requests/fast_forward_merge.md @@ -9,7 +9,7 @@ When the fast-forward merge ([`--ff-only`][ffonly]) setting is enabled, no merge commits will be created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. -When a fast-forward merge is not possible, the user must rebase the branch manually. +When a fast-forward merge is not possible, the user is given the option to rebase. ## Use cases @@ -25,7 +25,7 @@ merge commits. In such cases, the fast-forward merge is the perfect candidate. Now, when you visit the merge request page, you will be able to accept it **only if a fast-forward merge is possible**. -![Fast forward merge request](img/ff_merge_mr.png) +![Fast forward merge request](img/ff_merge_rebase.png) If the target branch is ahead of the source branch, you need to rebase the source branch locally before you will be able to do a fast-forward merge. diff --git a/doc/user/project/merge_requests/img/ff_merge_mr.png b/doc/user/project/merge_requests/img/ff_merge_mr.png Binary files differdeleted file mode 100644 index 241cc990343..00000000000 --- a/doc/user/project/merge_requests/img/ff_merge_mr.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/ff_merge_rebase.png b/doc/user/project/merge_requests/img/ff_merge_rebase.png Binary files differnew file mode 100644 index 00000000000..f6139f189ce --- /dev/null +++ b/doc/user/project/merge_requests/img/ff_merge_rebase.png diff --git a/features/project/ff_merge_requests.feature b/features/project/ff_merge_requests.feature index 995e52f9332..39035d551d1 100644 --- a/features/project/ff_merge_requests.feature +++ b/features/project/ff_merge_requests.feature @@ -22,3 +22,20 @@ Feature: Project Ff Merge Requests Then I should see ff-only merge button When I accept this merge request Then I should see merged request + + @javascript + Scenario: I do rebase before ff-only merge + Given ff merge enabled + And rebase before merge enabled + When I visit merge request page "Bug NS-05" + Then I should see rebase button + When I press rebase button + Then I should see rebase in progress message + + @javascript + Scenario: I do rebase before regular merge + Given rebase before merge enabled + When I visit merge request page "Bug NS-05" + Then I should see rebase button + When I press rebase button + Then I should see rebase in progress message diff --git a/features/steps/project/ff_merge_requests.rb b/features/steps/project/ff_merge_requests.rb index d68fe71e16e..27efcfd65b6 100644 --- a/features/steps/project/ff_merge_requests.rb +++ b/features/steps/project/ff_merge_requests.rb @@ -17,6 +17,10 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps author: project.users.first) end + step 'merge request is mergeable' do + expect(page).to have_button 'Merge' + end + step 'I should see ff-only merge button' do expect(page).to have_content "Fast-forward merge without a merge commit" expect(page).to have_button 'Merge' @@ -45,6 +49,10 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps project.save! end + step 'I should see rebase button' do + expect(page).to have_button "Rebase" + end + step 'merge request "Bug NS-05" is rebased' do merge_request.source_branch = 'flatten-dir' merge_request.target_branch = 'improve/awesome' @@ -59,6 +67,20 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps merge_request.save! end + step 'rebase before merge enabled' do + project = merge_request.target_project + project.merge_requests_rebase_enabled = true + project.save! + end + + step 'I press rebase button' do + click_button "Rebase" + end + + step "I should see rebase in progress message" do + expect(page).to have_content("Rebase in progress") + end + def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") end diff --git a/lib/api/api.rb b/lib/api/api.rb index 8094597d238..e0d14281c96 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -119,6 +119,7 @@ module API mount ::API::Features mount ::API::Files mount ::API::Groups + mount ::API::GroupMilestones mount ::API::Internal mount ::API::Issues mount ::API::Jobs @@ -129,8 +130,6 @@ module API mount ::API::Members mount ::API::MergeRequestDiffs mount ::API::MergeRequests - mount ::API::ProjectMilestones - mount ::API::GroupMilestones mount ::API::Namespaces mount ::API::Notes mount ::API::NotificationSettings @@ -139,6 +138,7 @@ module API mount ::API::PipelineSchedules mount ::API::ProjectHooks mount ::API::Projects + mount ::API::ProjectMilestones mount ::API::ProjectSnippets mount ::API::ProtectedBranches mount ::API::Repositories diff --git a/lib/api/boards.rb b/lib/api/boards.rb index 366b0dc9a6f..6c706b2b4e1 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -1,45 +1,46 @@ module API class Boards < Grape::API + include BoardsResponses include PaginationParams before { authenticate! } + helpers do + def board_parent + user_project + end + end + params do requires :id, type: String, desc: 'The ID of a project' end resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do - desc 'Get all project boards' do - detail 'This feature was introduced in 8.13' - success Entities::Board - end - params do - use :pagination - end - get ':id/boards' do - authorize!(:read_board, user_project) - present paginate(user_project.boards), with: Entities::Board + segment ':id/boards' do + desc 'Get all project boards' do + detail 'This feature was introduced in 8.13' + success Entities::Board + end + params do + use :pagination + end + get '/' do + authorize!(:read_board, user_project) + present paginate(board_parent.boards), with: Entities::Board + end + + desc 'Find a project board' do + detail 'This feature was introduced in 10.4' + success Entities::Board + end + get '/:board_id' do + present board, with: Entities::Board + end end params do requires :board_id, type: Integer, desc: 'The ID of a board' end segment ':id/boards/:board_id' do - helpers do - def project_board - board = user_project.boards.first - - if params[:board_id] == board.id - board - else - not_found!('Board') - end - end - - def board_lists - project_board.lists.destroyable - end - end - desc 'Get the lists of a project board' do detail 'Does not include `done` list. This feature was introduced in 8.13' success Entities::List @@ -72,22 +73,13 @@ module API requires :label_id, type: Integer, desc: 'The ID of an existing label' end post '/lists' do - unless available_labels.exists?(params[:label_id]) + unless available_labels_for(user_project).exists?(params[:label_id]) render_api_error!({ error: 'Label not found!' }, 400) end authorize!(:admin_list, user_project) - service = ::Boards::Lists::CreateService.new(user_project, current_user, - { label_id: params[:label_id] }) - - list = service.execute(project_board) - - if list.valid? - present list, with: Entities::List - else - render_validation_error!(list) - end + create_list end desc 'Moves a board list to a new position' do @@ -99,18 +91,11 @@ module API requires :position, type: Integer, desc: 'The position of the list' end put '/lists/:list_id' do - list = project_board.lists.movable.find(params[:list_id]) + list = board_lists.find(params[:list_id]) authorize!(:admin_list, user_project) - service = ::Boards::Lists::MoveService.new(user_project, current_user, - { position: params[:position] }) - - if service.execute(list) - present list, with: Entities::List - else - render_api_error!({ error: "List could not be moved!" }, 400) - end + move_list(list) end desc 'Delete a board list' do @@ -124,12 +109,7 @@ module API authorize!(:admin_list, user_project) list = board_lists.find(params[:list_id]) - destroy_conditionally!(list) do |list| - service = ::Boards::Lists::DestroyService.new(user_project, current_user) - unless service.execute(list) - render_api_error!({ error: 'List could not be deleted!' }, 400) - end - end + destroy_list(list) end end end diff --git a/lib/api/boards_responses.rb b/lib/api/boards_responses.rb new file mode 100644 index 00000000000..ead0943a74d --- /dev/null +++ b/lib/api/boards_responses.rb @@ -0,0 +1,50 @@ +module API + module BoardsResponses + extend ActiveSupport::Concern + + included do + helpers do + def board + board_parent.boards.find(params[:board_id]) + end + + def board_lists + board.lists.destroyable + end + + def create_list + create_list_service = + ::Boards::Lists::CreateService.new(board_parent, current_user, { label_id: params[:label_id] }) + + list = create_list_service.execute(board) + + if list.valid? + present list, with: Entities::List + else + render_validation_error!(list) + end + end + + def move_list(list) + move_list_service = + ::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i }) + + if move_list_service.execute(list) + present list, with: Entities::List + else + render_api_error!({ error: "List could not be moved!" }, 400) + end + end + + def destroy_list(list) + destroy_conditionally!(list) do |list| + service = ::Boards::Lists::DestroyService.new(board_parent, current_user) + unless service.execute(list) + render_api_error!({ error: 'List could not be deleted!' }, 400) + end + end + end + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index f5fa5fef389..bd0c54a1b04 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -791,6 +791,8 @@ module API class Board < Grape::Entity expose :id + expose :project, using: Entities::BasicProjectDetails + expose :lists, using: Entities::List do |board| board.lists.destroyable end @@ -1135,6 +1137,7 @@ module API class PagesDomainBasic < Grape::Entity expose :domain expose :url + expose :project_id expose :certificate, as: :certificate_expiration, if: ->(pages_domain, _) { pages_domain.certificate? }, diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 8ad4b2ecbf3..bf388163ec8 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -74,8 +74,15 @@ module API page || not_found!('Wiki Page') end - def available_labels - @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute + def available_labels_for(label_parent) + search_params = + if label_parent.is_a?(Project) + { project_id: label_parent.id } + else + { group_id: label_parent.id, only_group_labels: true } + end + + LabelsFinder.new(current_user, search_params).execute end def find_user(id) @@ -141,7 +148,9 @@ module API end def find_project_label(id) - label = available_labels.find_by_id(id) || available_labels.find_by_title(id) + labels = available_labels_for(user_project) + label = labels.find_by_id(id) || labels.find_by_title(id) + label || not_found!('Label') end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index b29c5848aef..7aa10631d53 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -277,6 +277,19 @@ module API present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project end + desc 'List participants for an issue' do + success Entities::UserBasic + end + params do + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' + end + get ':id/issues/:issue_iid/participants' do + issue = find_project_issue(params[:issue_iid]) + participants = ::Kaminari.paginate_array(issue.participants) + + present paginate(participants), with: Entities::UserBasic, current_user: current_user, project: user_project + end + desc 'Get the user agent details for an issue' do success Entities::UserAgentDetail end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index e41a1720ac1..81eaf56e48e 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -15,7 +15,7 @@ module API use :pagination end get ':id/labels' do - present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project + present paginate(available_labels_for(user_project)), with: Entities::Label, current_user: current_user, project: user_project end desc 'Create a new label' do @@ -30,7 +30,7 @@ module API post ':id/labels' do authorize! :admin_label, user_project - label = available_labels.find_by(title: params[:name]) + label = available_labels_for(user_project).find_by(title: params[:name]) conflict!('Label already exists') if label priority = params.delete(:priority) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 02f2b75ab9d..8f665b39fa8 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -185,6 +185,16 @@ module API present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project end + desc 'Get the participants of a merge request' do + success Entities::UserBasic + end + get ':id/merge_requests/:merge_request_iid/participants' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) + participants = ::Kaminari.paginate_array(merge_request.participants) + + present paginate(participants), with: Entities::UserBasic + end + desc 'Get the commits of a merge request' do success Entities::Commit end diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb index bd5eb2175e8..4157462ec2a 100644 --- a/lib/api/v3/labels.rb +++ b/lib/api/v3/labels.rb @@ -11,7 +11,7 @@ module API success ::API::Entities::Label end get ':id/labels' do - present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project + present available_labels_for(user_project), with: ::API::Entities::Label, current_user: current_user, project: user_project end desc 'Delete an existing label' do diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 0135b3c6f22..dd5d35feab9 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -15,6 +15,11 @@ module Gitlab execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all)) end + def git_clone_bundle(repo_path:, bundle_path:) + execute(%W(#{git_bin_path} clone --bare -- #{bundle_path} #{repo_path})) + Gitlab::Git::Repository.create_hooks(repo_path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path)) + end + def mkdir_p(path) FileUtils.mkdir_p(path, mode: DEFAULT_MODE) FileUtils.chmod(DEFAULT_MODE, path) diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 32ca2809b2f..d0e5cfcfd3e 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -13,7 +13,7 @@ module Gitlab def restore return true unless File.exist?(@path_to_bundle) - gitlab_shell.import_repository(@project.repository_storage_path, @project.disk_path, @path_to_bundle) + git_clone_bundle(repo_path: @project.repository.path_to_repo, bundle_path: @path_to_bundle) rescue => e @shared.error(e) false diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index b9cc97c9244..564047bbd34 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -101,6 +101,10 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/874 def import_repository(storage, name, url) + if url.start_with?('.', '/') + raise Error.new("don't use disk paths with import_repository: #{url.inspect}") + end + # The timeout ensures the subprocess won't hang forever cmd = gitlab_projects(storage, "#{name}.git") success = cmd.import_project(url, git_timeout) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 45c424af8c4..c8cc6b374f6 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -684,4 +684,62 @@ describe Projects::MergeRequestsController do format: :json end end + + describe 'POST #rebase' do + let(:viewer) { user } + + def post_rebase + post :rebase, namespace_id: project.namespace, project_id: project, id: merge_request + end + + def expect_rebase_worker_for(user) + expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id) + end + + context 'successfully' do + it 'enqeues a RebaseWorker' do + expect_rebase_worker_for(viewer) + + post_rebase + + expect(response.status).to eq(200) + end + end + + context 'with a forked project' do + let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:fork_owner) { fork_project.owner } + + before do + merge_request.update!(source_project: fork_project) + fork_project.add_reporter(user) + end + + context 'user cannot push to source branch' do + it 'returns 404' do + expect_rebase_worker_for(viewer).never + + post_rebase + + expect(response.status).to eq(404) + end + end + + context 'user can push to source branch' do + before do + project.add_reporter(fork_owner) + + sign_in(fork_owner) + end + + it 'returns 200' do + expect_rebase_worker_for(fork_owner) + + post_rebase + + expect(response.status).to eq(200) + end + end + end + end end diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index d507af3fd3d..06031aee217 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -56,6 +56,16 @@ describe LabelsFinder do expect(finder.execute).to eq [group_label_2, group_label_1, project_label_5] end + + context 'when only_group_labels is true' do + it 'returns only group labels' do + group_1.add_developer(user) + + finder = described_class.new(user, group_id: group_1.id, only_group_labels: true) + + expect(finder.execute).to eq [group_label_2, group_label_1] + end + end end context 'filtering by project_id' do diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json index 995f13381ad..f1199468d53 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_basic.json +++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json @@ -9,6 +9,7 @@ "human_time_estimate": { "type": ["string", "null"] }, "human_total_time_spent": { "type": ["string", "null"] }, "merge_error": { "type": ["string", "null"] }, + "rebase_in_progress": { "type": "boolean" }, "assignee_id": { "type": ["integer", "null"] }, "subscribed": { "type": ["boolean", "null"] }, "participants": { "type": "array" } diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index 9de27bee751..7f662098216 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -103,7 +103,11 @@ "remove_source_branch": { "type": ["boolean", "null"] }, "merge_ongoing": { "type": "boolean" }, "ff_only_enabled": { "type": ["boolean", false] }, - "should_be_rebased": { "type": "boolean" } + "should_be_rebased": { "type": "boolean" }, + "rebase_commit_sha": { "type": ["string", "null"] }, + "rebase_in_progress": { "type": "boolean" }, + "can_push_to_source_branch": { "type": "boolean" }, + "rebase_path": { "type": ["string", "null"] } }, "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/public_api/v4/board.json b/spec/fixtures/api/schemas/public_api/v4/board.json new file mode 100644 index 00000000000..d667f1d631c --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/board.json @@ -0,0 +1,86 @@ +{ + "type": "object", + "required" : [ + "id", + "project", + "lists" + ], + "properties" : { + "id": { "type": "integer" }, + "project": { + "type": ["object", "null"], + "required": [ + "id", + "avatar_url", + "description", + "default_branch", + "tag_list", + "ssh_url_to_repo", + "http_url_to_repo", + "web_url", + "name", + "name_with_namespace", + "path", + "path_with_namespace", + "star_count", + "forks_count", + "created_at", + "last_activity_at" + ], + "properties": { + "id": { "type": "integer" }, + "avatar_url": { "type": ["string", "null"] }, + "description": { "type": ["string", "null"] }, + "default_branch": { "type": ["string", "null"] }, + "tag_list": { "type": "array" }, + "ssh_url_to_repo": { "type": "string" }, + "http_url_to_repo": { "type": "string" }, + "web_url": { "type": "string" }, + "name": { "type": "string" }, + "name_with_namespace": { "type": "string" }, + "path": { "type": "string" }, + "path_with_namespace": { "type": "string" }, + "star_count": { "type": "integer" }, + "forks_count": { "type": "integer" }, + "created_at": { "type": "date" }, + "last_activity_at": { "type": "date" } + }, + "additionalProperties": false + }, + "lists": { + "type": "array", + "items": { + "type": "object", + "required" : [ + "id", + "label", + "position" + ], + "properties" : { + "id": { "type": "integer" }, + "label": { + "type": ["object", "null"], + "required": [ + "id", + "color", + "description", + "name" + ], + "properties": { + "id": { "type": "integer" }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$" + }, + "description": { "type": ["string", "null"] }, + "name": { "type": "string" } + } + }, + "position": { "type": ["integer", "null"] } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": true +} diff --git a/spec/fixtures/api/schemas/public_api/v4/boards.json b/spec/fixtures/api/schemas/public_api/v4/boards.json new file mode 100644 index 00000000000..117564ef77a --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/boards.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "board.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json index 4ba6422406c..e8c17298b43 100644 --- a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json @@ -3,6 +3,7 @@ "properties": { "domain": { "type": "string" }, "url": { "type": "uri" }, + "project_id": { "type": "integer" }, "certificate_expiration": { "type": "object", "properties": { @@ -13,6 +14,6 @@ "additionalProperties": false } }, - "required": ["domain", "url"], + "required": ["domain", "url", "project_id"], "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basic.json b/spec/fixtures/api/schemas/public_api/v4/user/basic.json index 9f69d31971c..bf330d8278c 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/basic.json @@ -1,5 +1,5 @@ { - "type": "object", + "type": ["object", "null"], "required": [ "id", "state", diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 1f46c225071..6f8dad6b835 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -62,4 +62,14 @@ describe('text_utility', () => { expect(textUtils.slugify('João')).toEqual('joão'); }); }); + + describe('stripeHtml', () => { + it('replaces html tag with the default replacement', () => { + expect(textUtils.stripeHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.'); + }); + + it('replaces html tags with the provided replacement', () => { + expect(textUtils.stripeHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .'); + }); + }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js new file mode 100644 index 00000000000..66ecaa316c8 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js @@ -0,0 +1,115 @@ +import Vue from 'vue'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import component from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Merge request widget rebase component', () => { + let Component; + let vm; + beforeEach(() => { + Component = Vue.extend(component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('While rebasing', () => { + it('should show progress message', () => { + vm = mountComponent(Component, { + mr: { rebaseInProgress: true }, + service: {}, + }); + + expect( + vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(), + ).toContain('Rebase in progress'); + }); + }); + + describe('With permissions', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + }, + service: {}, + }); + }); + + it('it should render rebase button and warning message', () => { + const text = vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(); + expect(text).toContain('Fast-forward merge is not possible.'); + expect(text).toContain('Rebase the source branch onto the target branch or merge target'); + expect(text).toContain('branch into source branch to allow this merge request to be merged.'); + }); + + it('it should render error message when it fails', (done) => { + vm.rebasingError = 'Something went wrong!'; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(), + ).toContain('Something went wrong!'); + done(); + }); + }); + }); + + describe('Without permissions', () => { + it('should render a message explaining user does not have permissions', () => { + vm = mountComponent(Component, { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: false, + targetBranch: 'foo', + }, + service: {}, + }); + + const text = vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(); + + expect(text).toContain('Fast-forward merge is not possible.'); + expect(text).toContain('Rebase the source branch onto'); + expect(text).toContain('foo'); + expect(text).toContain('to allow this merge request to be merged.'); + }); + }); + + describe('methods', () => { + it('checkRebaseStatus', (done) => { + spyOn(eventHub, '$emit'); + vm = mountComponent(Component, { + mr: {}, + service: { + rebase() { + return Promise.resolve(); + }, + poll() { + return Promise.resolve({ + data: { + rebase_in_progress: false, + merge_error: null, + }, + }); + }, + }, + }); + + vm.rebase(); + + // Wait for the rebase request + vm.$nextTick() + // Wait for the polling request + .then(vm.$nextTick()) + // Wait for the eventHub to be called + .then(vm.$nextTick()) + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js new file mode 100644 index 00000000000..a33ab689dd1 --- /dev/null +++ b/spec/javascripts/vue_shared/components/expand_button_spec.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import expandButton from '~/vue_shared/components/expand_button.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('expand button', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(expandButton); + vm = mountComponent(Component, { + slots: { + expanded: '<p>Expanded!</p>', + }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a collpased button', () => { + expect(vm.$el.textContent.trim()).toEqual('...'); + }); + + it('hides expander on click', (done) => { + vm.$el.querySelector('button').click(); + vm.$nextTick(() => { + expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;'); + done(); + }); + }); +}); diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index d8ebd46faab..07b3e1c1758 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1903,4 +1903,50 @@ describe MergeRequest do end end end + + describe '#should_be_rebased?' do + let(:project) { create(:project, :repository) } + + it 'returns false for the same source and target branches' do + merge_request = create(:merge_request, source_project: project, target_project: project) + + expect(merge_request.should_be_rebased?).to be_falsey + end + end + + describe '#rebase_in_progress?' do + # Create merge request and project before we stub file calls + before do + subject + end + + it 'returns true when there is a current rebase directory' do + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:mtime).and_return(Time.now) + + expect(subject.rebase_in_progress?).to be_truthy + end + + it 'returns false when there is no rebase directory' do + allow(File).to receive(:exist?).and_return(false) + + expect(subject.rebase_in_progress?).to be_falsey + end + + it 'returns false when the rebase directory has expired' do + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:mtime).and_return(20.minutes.ago) + + expect(subject.rebase_in_progress?).to be_falsey + end + + it 'returns false when the source project has been removed' do + allow(subject).to receive(:source_project).and_return(nil) + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:mtime).and_return(Time.now) + + expect(File).not_to have_received(:exist?) + expect(subject.rebase_in_progress?).to be_falsey + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 0678cae9b93..b3f160f3119 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -250,9 +250,13 @@ describe Namespace do parent.update(path: 'mygroup_new') - expect(project_in_parent_group.repo.config['gitlab.fullpath']).to eq "mygroup_new/#{project_in_parent_group.path}" - expect(hashed_project_in_subgroup.repo.config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}" - expect(legacy_project_in_subgroup.repo.config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{legacy_project_in_subgroup.path}" + expect(project_rugged(project_in_parent_group).config['gitlab.fullpath']).to eq "mygroup_new/#{project_in_parent_group.path}" + expect(project_rugged(hashed_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}" + expect(project_rugged(legacy_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{legacy_project_in_subgroup.path}" + end + + def project_rugged(project) + project.repository.rugged end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 3c2ed043b82..32f40f8c365 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -418,14 +418,21 @@ describe Project do end describe '#merge_method' do - it 'returns "ff" merge_method when ff is enabled' do - project = build(:project, merge_requests_ff_only_enabled: true) - expect(project.merge_method).to be :ff + using RSpec::Parameterized::TableSyntax + + where(:ff, :rebase, :method) do + true | true | :ff + true | false | :ff + false | true | :rebase_merge + false | false | :merge end - it 'returns "merge" merge_method when ff is disabled' do - project = build(:project, merge_requests_ff_only_enabled: false) - expect(project.merge_method).to be :merge + with_them do + let(:project) { build(:project, merge_requests_rebase_enabled: rebase, merge_requests_ff_only_enabled: ff) } + + subject { project.merge_method } + + it { is_expected.to eq(method) } end end @@ -2632,7 +2639,7 @@ describe Project do project.rename_repo - expect(project.repo.config['gitlab.fullpath']).to eq(project.full_path) + expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path) end end @@ -2793,7 +2800,7 @@ describe Project do it 'updates project full path in .git/config' do project.rename_repo - expect(project.repo.config['gitlab.fullpath']).to eq(project.full_path) + expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path) end end @@ -3143,13 +3150,13 @@ describe Project do it 'writes full path in .git/config when key is missing' do project.write_repository_config - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end it 'updates full path in .git/config when key is present' do project.write_repository_config(gl_full_path: 'old/path') - expect { project.write_repository_config }.to change { project.repo.config['gitlab.fullpath'] }.from('old/path').to(project.full_path) + expect { project.write_repository_config }.to change { project.repository.rugged.config['gitlab.fullpath'] }.from('old/path').to(project.full_path) end it 'does not raise an error with an empty repository' do diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index 969c4753f33..e3b37739e8e 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -404,4 +404,67 @@ describe MergeRequestPresenter do .to eq("<a href=\"/#{resource.source_project.full_path}/tree/#{resource.source_branch}\">#{resource.source_branch}</a>") end end + + describe '#rebase_path' do + before do + allow(resource).to receive(:rebase_in_progress?) { rebase_in_progress } + allow(resource).to receive(:should_be_rebased?) { should_be_rebased } + + allow_any_instance_of(Gitlab::UserAccess::RequestCacheExtension) + .to receive(:can_push_to_branch?) + .with(resource.source_branch) + .and_return(can_push_to_branch) + end + + subject do + described_class.new(resource, current_user: user).rebase_path + end + + context 'when can rebase' do + let(:rebase_in_progress) { false } + let(:can_push_to_branch) { true } + let(:should_be_rebased) { true } + + before do + allow(resource).to receive(:source_branch_exists?) { true } + end + + it 'returns path' do + is_expected + .to eq("/#{project.full_path}/merge_requests/#{resource.iid}/rebase") + end + end + + context 'when cannot rebase' do + context 'when rebase in progress' do + let(:rebase_in_progress) { true } + let(:can_push_to_branch) { true } + let(:should_be_rebased) { true } + + it 'returns nil' do + is_expected.to be_nil + end + end + + context 'when user cannot merge' do + let(:rebase_in_progress) { false } + let(:can_push_to_branch) { false } + let(:should_be_rebased) { true } + + it 'returns nil' do + is_expected.to be_nil + end + end + + context 'should not be rebased' do + let(:rebase_in_progress) { false } + let(:can_push_to_branch) { true } + let(:should_be_rebased) { false } + + it 'returns nil' do + is_expected.to be_nil + end + end + end + end end diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index f65af69dc7f..c6c10025f7f 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -6,18 +6,18 @@ describe API::Boards do set(:non_member) { create(:user) } set(:guest) { create(:user) } set(:admin) { create(:user, :admin) } - set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } + set(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } set(:dev_label) do - create(:label, title: 'Development', color: '#FFAABB', project: project) + create(:label, title: 'Development', color: '#FFAABB', project: board_parent) end set(:test_label) do - create(:label, title: 'Testing', color: '#FFAACC', project: project) + create(:label, title: 'Testing', color: '#FFAACC', project: board_parent) end set(:ux_label) do - create(:label, title: 'UX', color: '#FF0000', project: project) + create(:label, title: 'UX', color: '#FF0000', project: board_parent) end set(:dev_list) do @@ -28,180 +28,25 @@ describe API::Boards do create(:list, label: test_label, position: 2) end - set(:board) do - create(:board, project: project, lists: [dev_list, test_list]) - end - - before do - project.add_reporter(user) - project.add_guest(guest) - end + set(:milestone) { create(:milestone, project: board_parent) } + set(:board_label) { create(:label, project: board_parent) } + set(:board) { create(:board, project: board_parent, lists: [dev_list, test_list]) } - describe "GET /projects/:id/boards" do - let(:base_url) { "/projects/#{project.id}/boards" } + it_behaves_like 'group and project boards', "/projects/:id/boards" - context "when unauthenticated" do - it "returns authentication error" do - get api(base_url) - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns the project issue board" do - get api(base_url, user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(board.id) - expect(json_response.first['lists']).to be_an Array - expect(json_response.first['lists'].length).to eq(2) - expect(json_response.first['lists'].last).to have_key('position') - end - end - end - - describe "GET /projects/:id/boards/:board_id/lists" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it 'returns issue board lists' do - get api(base_url, user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['label']['name']).to eq(dev_label.title) - end - - it 'returns 404 if board not found' do - get api("/projects/#{project.id}/boards/22343/lists", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "GET /projects/:id/boards/:board_id/lists/:list_id" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it 'returns a list' do - get api("#{base_url}/#{dev_list.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(dev_list.id) - expect(json_response['label']['name']).to eq(dev_label.title) - expect(json_response['position']).to eq(1) - end - - it 'returns 404 if list not found' do - get api("#{base_url}/5324", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/board/lists" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } + describe "POST /projects/:id/boards/lists" do + let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}/lists" } it 'creates a new issue board list for group labels' do group = create(:group) group_label = create(:group_label, group: group) - project.update(group: group) + board_parent.update(group: group) - post api(base_url, user), label_id: group_label.id + post api(url, user), label_id: group_label.id expect(response).to have_gitlab_http_status(201) expect(json_response['label']['name']).to eq(group_label.title) expect(json_response['position']).to eq(3) end - - it 'creates a new issue board list for project labels' do - post api(base_url, user), label_id: ux_label.id - - expect(response).to have_gitlab_http_status(201) - expect(json_response['label']['name']).to eq(ux_label.title) - expect(json_response['position']).to eq(3) - end - - it 'returns 400 when creating a new list if label_id is invalid' do - post api(base_url, user), label_id: 23423 - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 403 for project members with guest role' do - put api("#{base_url}/#{test_list.id}", guest), position: 1 - - expect(response).to have_gitlab_http_status(403) - end - end - - describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it "updates a list" do - put api("#{base_url}/#{test_list.id}", user), - position: 1 - - expect(response).to have_gitlab_http_status(200) - expect(json_response['position']).to eq(1) - end - - it "returns 404 error if list id not found" do - put api("#{base_url}/44444", user), - position: 1 - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 403 for project members with guest role" do - put api("#{base_url}/#{test_list.id}", guest), - position: 1 - - expect(response).to have_gitlab_http_status(403) - end - end - - describe "DELETE /projects/:id/board/lists/:list_id" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it "rejects a non member from deleting a list" do - delete api("#{base_url}/#{dev_list.id}", non_member) - - expect(response).to have_gitlab_http_status(403) - end - - it "rejects a user with guest role from deleting a list" do - delete api("#{base_url}/#{dev_list.id}", guest) - - expect(response).to have_gitlab_http_status(403) - end - - it "returns 404 error if list id not found" do - delete api("#{base_url}/44444", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "when the user is project owner" do - set(:owner) { create(:user) } - - before do - project.update(namespace: owner.namespace) - end - - it "deletes the list if an admin requests it" do - delete api("#{base_url}/#{dev_list.id}", owner) - - expect(response).to have_gitlab_http_status(204) - end - - it_behaves_like '412 response' do - let(:request) { api("#{base_url}/#{dev_list.id}", owner) } - end - end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 00d9c795619..320217f2032 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1582,4 +1582,16 @@ describe API::Issues, :mailer do expect(json_response).to be_an Array expect(json_response.length).to eq(size) if size end + + describe 'GET projects/:id/issues/:issue_iid/participants' do + it_behaves_like 'issuable participants endpoint' do + let(:entity) { issue } + end + + it 'returns 404 if the issue is confidential' do + post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member) + + expect(response).to have_gitlab_http_status(404) + end + end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index ef3f610740d..0c9fbb1f187 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -500,6 +500,12 @@ describe API::MergeRequests do end end + describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do + it_behaves_like 'issuable participants endpoint' do + let(:entity) { merge_request } + end + end + describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do it 'returns a 200 when merge request is valid' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user) diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index d412b045e9f..5d01dc37f0e 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -46,6 +46,7 @@ describe API::PagesDomains do expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.last).to have_key('domain') + expect(json_response.last).to have_key('project_id') expect(json_response.last).to have_key('certificate_expiration') expect(json_response.last['certificate_expiration']['expired']).to be true expect(json_response.first).not_to have_key('certificate_expiration') diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index e25552eb0d8..80a271ba7fb 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -190,4 +190,20 @@ describe MergeRequestWidgetEntity do end end end + + describe 'when source project is deleted' do + let(:project) { create(:project, :repository) } + let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) } + + it 'returns a blank rebase_path' do + allow(merge_request).to receive(:should_be_rebased?).and_return(true) + fork_project.destroy + merge_request.reload + + entity = described_class.new(merge_request, request: request).as_json + + expect(entity[:rebase_path]).to be_nil + end + end end diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb new file mode 100644 index 00000000000..d1b37cdd073 --- /dev/null +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe MergeRequests::RebaseService do + include ProjectForksHelper + + let(:user) { create(:user) } + let(:merge_request) do + create(:merge_request, + source_branch: 'feature_conflict', + target_branch: 'master') + end + let(:project) { merge_request.project } + let(:repository) { project.repository.raw } + + subject(:service) { described_class.new(project, user, {}) } + + before do + project.add_master(user) + end + + describe '#execute' do + context 'when another rebase is already in progress' do + before do + allow(merge_request).to receive(:rebase_in_progress?).and_return(true) + end + + it 'saves the error message' do + subject.execute(merge_request) + + expect(merge_request.reload.merge_error).to eq 'Rebase task canceled: Another rebase is already in progress' + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: 'Failed to rebase. Should be done manually') + end + end + + context 'when unexpected error occurs' do + before do + allow(repository).to receive(:run_git!).and_raise('Something went wrong') + end + + it 'saves the error message' do + subject.execute(merge_request) + + expect(merge_request.reload.merge_error).to eq 'Something went wrong' + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: 'Failed to rebase. Should be done manually') + end + end + + context 'with git command failure' do + before do + allow(repository).to receive(:run_git!).and_raise(Gitlab::Git::Repository::GitError, 'Something went wrong') + end + + it 'saves the error message' do + subject.execute(merge_request) + + expect(merge_request.reload.merge_error).to eq 'Something went wrong' + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: 'Failed to rebase. Should be done manually') + end + end + + context 'valid params' do + before do + service.execute(merge_request) + end + + it 'rebases source branch' do + parent_sha = merge_request.source_project.repository.commit(merge_request.source_branch).parents.first.sha + target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha + expect(parent_sha).to eq(target_branch_sha) + end + + it 'records the new SHA on the merge request' do + head_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha + expect(merge_request.reload.rebase_commit_sha).to eq(head_sha) + end + + it 'logs correct author and commiter' do + head_commit = merge_request.source_project.repository.commit(merge_request.source_branch) + + expect(head_commit.author_email).to eq('dmitriy.zaporozhets@gmail.com') + expect(head_commit.author_name).to eq('Dmitriy Zaporozhets') + expect(head_commit.committer_email).to eq(user.email) + expect(head_commit.committer_name).to eq(user.name) + end + + context 'git commands' do + it 'sets GL_REPOSITORY env variable when calling git commands' do + expect(repository).to receive(:popen).exactly(3) + .with(anything, anything, hash_including('GL_REPOSITORY')) + .and_return(['', 0]) + + service.execute(merge_request) + end + end + + context 'fork' do + let(:forked_project) do + fork_project(project, user, repository: true) + end + + let(:merge_request_from_fork) do + forked_project.repository.create_file( + user, + 'new-file-to-target', + '', + message: 'Add new file to target', + branch_name: 'master') + + create(:merge_request, + source_branch: 'master', source_project: forked_project, + target_branch: 'master', target_project: project) + end + + it 'rebases source branch' do + parent_sha = forked_project.repository.commit(merge_request_from_fork.source_branch).parents.first.sha + target_branch_sha = project.repository.commit(merge_request_from_fork.target_branch).sha + expect(parent_sha).to eq(target_branch_sha) + end + end + end + end +end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 1833078f37c..9a44dfde41b 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -255,7 +255,7 @@ describe Projects::CreateService, '#execute' do it 'writes project full path to .git/config' do project = create_project(user, opts) - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end def create_project(user, opts) diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb index ded864beb1d..7b536cc05cb 100644 --- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb @@ -37,7 +37,7 @@ describe Projects::HashedStorage::MigrateRepositoryService do it 'writes project full path to .git/config' do service.execute - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 7377c748698..39f6388c25e 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -58,7 +58,7 @@ describe Projects::TransferService do it 'updates project full path in .git/config' do transfer_project(project, user, group) - expect(project.repo.config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" + expect(project.repository.rugged.config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" end end @@ -95,7 +95,7 @@ describe Projects::TransferService do it 'rolls back project full path in .git/config' do attempt_project_transfer - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end it "doesn't send move notifications" do diff --git a/spec/support/api/boards_shared_examples.rb b/spec/support/api/boards_shared_examples.rb new file mode 100644 index 00000000000..943c1f6ffd7 --- /dev/null +++ b/spec/support/api/boards_shared_examples.rb @@ -0,0 +1,180 @@ +shared_examples_for 'group and project boards' do |route_definition, ee = false| + let(:root_url) { route_definition.gsub(":id", board_parent.id.to_s) } + + before do + board_parent.add_reporter(user) + board_parent.add_guest(guest) + end + + def expect_schema_match_for(response, schema_file, ee) + if ee + expect(response).to match_response_schema(schema_file, dir: "ee") + else + expect(response).to match_response_schema(schema_file) + end + end + + describe "GET #{route_definition}" do + context "when unauthenticated" do + it "returns authentication error" do + get api(root_url) + + expect(response).to have_gitlab_http_status(401) + end + end + + context "when authenticated" do + it "returns the issue boards" do + get api(root_url, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect_schema_match_for(response, 'public_api/v4/boards', ee) + end + + describe "GET #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } + + it 'get a single board by id' do + get api(url, user) + + expect_schema_match_for(response, 'public_api/v4/board', ee) + end + end + end + end + + describe "GET #{route_definition}/:board_id/lists" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'returns issue board lists' do + get api(url, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['label']['name']).to eq(dev_label.title) + end + + it 'returns 404 if board not found' do + get api("#{root_url}/22343/lists", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe "GET #{route_definition}/:board_id/lists/:list_id" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'returns a list' do + get api("#{url}/#{dev_list.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['id']).to eq(dev_list.id) + expect(json_response['label']['name']).to eq(dev_label.title) + expect(json_response['position']).to eq(1) + end + + it 'returns 404 if list not found' do + get api("#{url}/5324", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe "POST #{route_definition}/lists" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'creates a new issue board list for labels' do + post api(url, user), label_id: ux_label.id + + expect(response).to have_gitlab_http_status(201) + expect(json_response['label']['name']).to eq(ux_label.title) + expect(json_response['position']).to eq(3) + end + + it 'returns 400 when creating a new list if label_id is invalid' do + post api(url, user), label_id: 23423 + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 403 for members with guest role' do + put api("#{url}/#{test_list.id}", guest), position: 1 + + expect(response).to have_gitlab_http_status(403) + end + end + + describe "PUT #{route_definition}/:board_id/lists/:list_id to update only position" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it "updates a list" do + put api("#{url}/#{test_list.id}", user), + position: 1 + + expect(response).to have_gitlab_http_status(200) + expect(json_response['position']).to eq(1) + end + + it "returns 404 error if list id not found" do + put api("#{url}/44444", user), + position: 1 + + expect(response).to have_gitlab_http_status(404) + end + + it "returns 403 for members with guest role" do + put api("#{url}/#{test_list.id}", guest), + position: 1 + + expect(response).to have_gitlab_http_status(403) + end + end + + describe "DELETE #{route_definition}/lists/:list_id" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it "rejects a non member from deleting a list" do + delete api("#{url}/#{dev_list.id}", non_member) + + expect(response).to have_gitlab_http_status(403) + end + + it "rejects a user with guest role from deleting a list" do + delete api("#{url}/#{dev_list.id}", guest) + + expect(response).to have_gitlab_http_status(403) + end + + it "returns 404 error if list id not found" do + delete api("#{url}/44444", user) + + expect(response).to have_gitlab_http_status(404) + end + + context "when the user is parent owner" do + set(:owner) { create(:user) } + + before do + if board_parent.try(:namespace) + board_parent.update(namespace: owner.namespace) + else + board.parent.add_owner(owner) + end + end + + it "deletes the list if an admin requests it" do + delete api("#{url}/#{dev_list.id}", owner) + + expect(response).to have_gitlab_http_status(204) + end + + it_behaves_like '412 response' do + let(:request) { api("#{url}/#{dev_list.id}", owner) } + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb new file mode 100644 index 00000000000..96d59e0c472 --- /dev/null +++ b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb @@ -0,0 +1,29 @@ +shared_examples 'issuable participants endpoint' do + let(:area) { entity.class.name.underscore.pluralize } + + it 'returns participants' do + get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(entity.participants.size) + + last_participant = entity.participants.last + expect(json_response.last['id']).to eq(last_participant.id) + expect(json_response.last['name']).to eq(last_participant.name) + expect(json_response.last['username']).to eq(last_participant.username) + end + + it 'returns a 404 when iid does not exist' do + get api("/projects/#{project.id}/#{area}/999/participants", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns a 404 when id is used instead of iid' do + get api("/projects/#{project.id}/#{area}/#{entity.id}/participants", user) + + expect(response).to have_gitlab_http_status(404) + end +end diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index 28d54c2fb77..264e0ce0b40 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -54,6 +54,8 @@ describe 'projects/merge_requests/show.html.haml' do it 'closes the merge request if the source project does not exist' do closed_merge_request.update_attributes(state: 'open') forked_project.destroy + # Reload merge request so MergeRequest#source_project turns to `nil` + closed_merge_request.reload render diff --git a/spec/workers/rebase_worker_spec.rb b/spec/workers/rebase_worker_spec.rb new file mode 100644 index 00000000000..20aff020dbb --- /dev/null +++ b/spec/workers/rebase_worker_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe RebaseWorker, '#perform' do + context 'when rebasing an MR from a fork where upstream has protected branches' do + let(:upstream_project) { create(:project, :repository) } + let(:fork_project) { create(:project, :repository) } + + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + source_branch: 'feature_conflict', + target_project: upstream_project, + target_branch: 'master') + end + + before do + create(:forked_project_link, forked_to_project: fork_project, forked_from_project: upstream_project) + end + + it 'sets the correct project for running hooks' do + expect(MergeRequests::RebaseService) + .to receive(:new).with(fork_project, merge_request.author).and_call_original + + subject.perform(merge_request, merge_request.author) + end + end +end diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index 18910a46d11..06473fba8e1 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -34,6 +34,10 @@ variables: POSTGRES_ENABLED: "true" POSTGRES_DB: $CI_ENVIRONMENT_SLUG + KUBERNETES_VERSION: 1.8.6 + HELM_VERSION: 2.6.1 + CODECLIMATE_VERSION: 0.69.0 + stages: - build - test @@ -250,8 +254,8 @@ production: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume /tmp/cc:/tmp/cc" - docker run ${cc_opts} codeclimate/codeclimate:0.69.0 init - docker run ${cc_opts} codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json + docker run ${cc_opts} "codeclimate/codeclimate:${CODECLIMATE_VERSION}" init + docker run ${cc_opts} "codeclimate/codeclimate:${CODECLIMATE_VERSION}" analyze -f json > codeclimate.json } function sast() { @@ -323,11 +327,11 @@ production: apk add glibc-2.23-r3.apk rm glibc-2.23-r3.apk - curl https://kubernetes-helm.storage.googleapis.com/helm-v2.6.1-linux-amd64.tar.gz | tar zx + curl "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx mv linux-amd64/helm /usr/bin/ helm version --client - curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl + curl -L -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl" chmod +x /usr/bin/kubectl kubectl version --client } |