diff options
94 files changed, 1445 insertions, 253 deletions
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 9084fa2f716..26aaba0e866 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.1.0 +1.2.0 diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 73b2cd0b2c7..95636a9ccdd 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -15,6 +15,7 @@ export const defaultAutocompleteConfig = { epics: true, milestones: true, labels: true, + snippets: true, }; class GfmAutoComplete { @@ -50,6 +51,7 @@ class GfmAutoComplete { if (this.enableMap.milestones) this.setupMilestones($input); if (this.enableMap.mergeRequests) this.setupMergeRequests($input); if (this.enableMap.labels) this.setupLabels($input); + if (this.enableMap.snippets) this.setupSnippets($input); // We don't instantiate the quick actions autocomplete for note and issue/MR edit forms $input.filter('[data-supports-quick-actions="true"]').atwho({ @@ -360,6 +362,39 @@ class GfmAutoComplete { }); } + setupSnippets($input) { + $input.atwho({ + at: '$', + alias: 'snippets', + searchKey: 'search', + displayTpl(value) { + let tmpl = GfmAutoComplete.Loading.template; + if (value.title != null) { + tmpl = GfmAutoComplete.Issues.template; + } + return tmpl; + }, + data: GfmAutoComplete.defaultLoadingData, + // eslint-disable-next-line no-template-curly-in-string + insertTpl: '${atwho-at}${id}', + callbacks: { + ...this.getDefaultCallbacks(), + beforeSave(snippets) { + return $.map(snippets, (m) => { + if (m.title == null) { + return m; + } + return { + id: m.id, + title: sanitize(m.title), + search: `${m.id} ${m.title}`, + }; + }); + }, + }, + }); + } + getDefaultCallbacks() { const fetchData = this.fetchData.bind(this); @@ -470,7 +505,7 @@ class GfmAutoComplete { // The below is taken from At.js source // Tweaked to commands to start without a space only if char before is a non-word character // https://github.com/ichord/At.js - const atSymbolsWithBar = Object.keys(controllers).join('|'); + const atSymbolsWithBar = Object.keys(controllers).join('|').replace(/[$]/, '\\$&'); const atSymbolsWithoutBar = Object.keys(controllers).join(''); const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop(); const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); @@ -497,6 +532,7 @@ GfmAutoComplete.atTypeMap = { '~': 'labels', '%': 'milestones', '/': 'commands', + $: 'snippets', }; // Emoji @@ -519,7 +555,7 @@ GfmAutoComplete.Labels = { // eslint-disable-next-line no-template-curly-in-string template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>', }; -// Issues and MergeRequests +// Issues, MergeRequests and Snippets GfmAutoComplete.Issues = { // eslint-disable-next-line no-template-curly-in-string template: '<li><small>${id}</small> ${title}</li>', diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index bcf8686afcc..c3d39082714 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -66,7 +66,7 @@ <button :class="{ disabled: formState.updateLoading || !isSubmitEnabled }" :disabled="formState.updateLoading || !isSubmitEnabled" - class="btn btn-success float-left" + class="btn btn-success float-left qa-save-button" type="submit" @click.prevent="updateIssuable"> Save changes @@ -86,7 +86,7 @@ v-if="shouldShowDeleteButton" :class="{ disabled: deleteLoading }" :disabled="deleteLoading" - class="btn btn-danger float-right append-right-default" + class="btn btn-danger float-right append-right-default qa-delete-button" type="button" @click="deleteIssuable"> Delete diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 97acc5ba385..1a78c59d715 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -61,7 +61,8 @@ ref="textarea" slot="textarea" v-model="formState.description" - class="note-textarea js-gfm-input js-autosize markdown-area" + class="note-textarea js-gfm-input js-autosize markdown-area + qa-description-textarea" data-supports-quick-actions="false" aria-label="Description" placeholder="Write a comment or drag your files here…" diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue index 7d1526a64b4..b7f2b1a6050 100644 --- a/app/assets/javascripts/issue_show/components/fields/title.vue +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -22,7 +22,7 @@ <input id="issuable-title" v-model="formState.title" - class="form-control" + class="form-control qa-title-input" type="text" placeholder="Title" aria-label="Title" diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index cf99e9a9cd8..ed26e53ac0e 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -79,7 +79,8 @@ export default { v-if="showInlineEditButton && canUpdate" v-tooltip type="button" - class="btn btn-default btn-edit btn-svg js-issuable-edit" + class="btn btn-default btn-edit btn-svg js-issuable-edit + qa-edit-button" title="Edit title and description" data-placement="bottom" data-container="body" diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 7735133c470..b980e43b898 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -7,7 +7,11 @@ import { __, sprintf } from '~/locale'; import Flash from '../../flash'; import Autosave from '../../autosave'; import TaskList from '../../task_list'; -import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase } from '../../lib/utils/text_utility'; +import { + capitalizeFirstCharacter, + convertToCamelCase, + splitCamelCase, +} from '../../lib/utils/text_utility'; import * as constants from '../constants'; import eventHub from '../event_hub'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; @@ -122,7 +126,9 @@ export default { return this.getNoteableData.create_note_path; }, issuableTypeTitle() { - return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE ? 'merge request' : 'issue'; + return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE + ? 'merge request' + : 'issue'; }, }, watch: { @@ -359,7 +365,7 @@ Please check your network connection and try again.`; :disabled="isSubmitting" name="note[note]" class="note-textarea js-vue-comment-form js-note-text -js-gfm-input js-autosize markdown-area js-vue-textarea" +js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" data-supports-quick-actions="true" aria-label="Description" placeholder="Write a comment or drag your files here…" @@ -374,7 +380,8 @@ js-gfm-input js-autosize markdown-area js-vue-textarea" append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button :disabled="isSubmitButtonDisabled" - class="btn btn-success comment-btn js-comment-button js-comment-submit-button" + class="btn btn-create comment-btn js-comment-button js-comment-submit-button + qa-comment-button" type="submit" @click.prevent="handleSave()"> {{ __(commentButtonTitle) }} diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 875f6928bed..a16f7e6b77c 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -52,6 +52,21 @@ required: false, default: '', }, + pagesAvailable: { + type: Boolean, + required: false, + default: false, + }, + pagesAccessControlEnabled: { + type: Boolean, + required: false, + default: false, + }, + pagesHelpPath: { + type: String, + required: false, + default: '', + }, }, data() { @@ -64,6 +79,7 @@ buildsAccessLevel: 20, wikiAccessLevel: 20, snippetsAccessLevel: 20, + pagesAccessLevel: 20, containerRegistryEnabled: true, lfsEnabled: true, requestAccessEnabled: true, @@ -90,6 +106,13 @@ ); }, + pagesFeatureAccessLevelOptions() { + if (this.visibilityLevel !== visibilityOptions.PUBLIC) { + return this.featureAccessLevelOptions.concat([[30, 'Everyone']]); + } + return this.featureAccessLevelOptions; + }, + repositoryEnabled() { return this.repositoryAccessLevel > 0; }, @@ -109,6 +132,10 @@ this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); + if (this.pagesAccessLevel === 20) { + // When from Internal->Private narrow access for only members + this.pagesAccessLevel = 10; + } this.highlightChanges(); } else if (oldValue === visibilityOptions.PRIVATE) { // if changing away from private, make enabled features more permissive @@ -118,6 +145,7 @@ if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; + if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20; this.highlightChanges(); } }, @@ -323,6 +351,18 @@ name="project[project_feature_attributes][snippets_access_level]" /> </project-setting-row> + <project-setting-row + v-if="pagesAvailable && pagesAccessControlEnabled" + :help-path="pagesHelpPath" + label="Pages" + help-text="Static website for the project." + > + <project-feature-setting + v-model="pagesAccessLevel" + :options="pagesFeatureAccessLevelOptions" + name="project[project_feature_attributes][pages_access_level]" + /> + </project-setting-row> </div> </div> </template> diff --git a/app/assets/javascripts/pages/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js index f369c7ef9a6..8859557e62d 100644 --- a/app/assets/javascripts/pages/snippets/form.js +++ b/app/assets/javascripts/pages/snippets/form.js @@ -11,6 +11,7 @@ export default () => { epics: false, milestones: false, labels: false, + snippets: false, }); new ZenMode(); // eslint-disable-line no-new }; diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js index 8681a1776c6..0ff84dc4667 100644 --- a/app/assets/javascripts/shared/milestones/form.js +++ b/app/assets/javascripts/shared/milestones/form.js @@ -15,5 +15,6 @@ export default (initGFM = true) => { epics: initGFM, milestones: initGFM, labels: initGFM, + snippets: initGFM, }); }; diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index d62537021ca..10e8ddad9cd 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -76,6 +76,7 @@ epics: this.enableAutocomplete, milestones: this.enableAutocomplete, labels: this.enableAutocomplete, + snippets: this.enableAutocomplete, }); }, beforeDestroy() { diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index 7c93cf36862..d386fb63d9f 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -27,6 +27,10 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController render json: @autocomplete_service.commands(target, params[:type]) end + def snippets + render json: @autocomplete_service.snippets + end + private def load_autocomplete_service diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a9417369ca2..ee438e160f2 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -366,6 +366,7 @@ class ProjectsController < Projects::ApplicationController repository_access_level snippets_access_level wiki_access_level + pages_access_level ] ] end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 9e24154e4b6..1f98ecf95ca 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -363,6 +363,7 @@ class IssuableFinder def use_cte_for_search? return false unless search return false unless Gitlab::Database.postgresql? + return false unless Feature.enabled?(:use_cte_for_group_issues_search, default_enabled: true) params[:use_cte_for_search] end @@ -428,6 +429,10 @@ class IssuableFinder params[:milestone_title] == Milestone::Upcoming.name end + def filter_by_any_milestone? + params[:milestone_title] == Milestone::Any.title + end + def filter_by_started_milestone? params[:milestone_title] == Milestone::Started.name end @@ -437,6 +442,8 @@ class IssuableFinder if milestones? if filter_by_no_milestone? items = items.left_joins_milestones.where(milestone_id: [-1, nil]) + elsif filter_by_any_milestone? + items = items.any_milestone elsif filter_by_upcoming_milestone? upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items)) items = items.left_joins_milestones.where(milestone_id: upcoming_ids) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 770e0bfe1a3..abdc47b9866 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -120,9 +120,13 @@ class IssuesFinder < IssuableFinder return @user_can_see_all_confidential_issues = true if current_user.full_private_access? @user_can_see_all_confidential_issues = - project? && - project && - project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL + if project? && project + project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL + elsif group + group.max_member_access_for_user(current_user) >= CONFIDENTIAL_ACCESS_LEVEL + else + false + end end def user_cannot_see_confidential_issues? diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 82e0b2ed9e1..d000af21be3 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -129,7 +129,7 @@ class LabelsFinder < UnionFinder end def project? - params[:project_id].present? + params[:project].present? || params[:project_id].present? end def projects? @@ -152,7 +152,7 @@ class LabelsFinder < UnionFinder return @project if defined?(@project) if project? - @project = Project.find(params[:project_id]) + @project = params[:project] || Project.find(params[:project_id]) @project = nil unless authorized_to_read_labels?(@project) else @project = nil diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb index 066ce64a254..ab37c282fe5 100644 --- a/app/graphql/types/permission_types/project.rb +++ b/app/graphql/types/permission_types/project.rb @@ -16,7 +16,7 @@ module Types :create_deployment, :push_to_delete_protected_branch, :admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki, - :create_pages, :destroy_pages + :create_pages, :destroy_pages, :read_pages_content end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 32fc8e5e9ce..4f91e3e4117 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -292,7 +292,8 @@ module ApplicationHelper mergeRequests: merge_requests_project_autocomplete_sources_path(object), labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]), milestones: milestones_project_autocomplete_sources_path(object), - commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]) + commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]), + snippets: snippets_project_autocomplete_sources_path(object) } end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 8b17e6ef75d..0016f89db5c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -454,6 +454,7 @@ module ProjectsHelper buildsAccessLevel: feature.builds_access_level, wikiAccessLevel: feature.wiki_access_level, snippetsAccessLevel: feature.snippets_access_level, + pagesAccessLevel: feature.pages_access_level, containerRegistryEnabled: !!project.container_registry_enabled, lfsEnabled: !!project.lfs_enabled } @@ -468,7 +469,10 @@ module ProjectsHelper registryAvailable: Gitlab.config.registry.enabled, registryHelpPath: help_page_path('user/project/container_registry'), lfsAvailable: Gitlab.config.lfs.enabled, - lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs'), + pagesAvailable: Gitlab.config.pages.enabled, + pagesAccessControlEnabled: Gitlab.config.pages.access_control, + pagesHelpPath: help_page_path('user/project/pages/index.md') } end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d986e33280d..17024e8a0af 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -633,6 +633,18 @@ module Ci end end + def branch_updated? + strong_memoize(:branch_updated) do + push_details.branch_updated? + end + end + + def modified_paths + strong_memoize(:modified_paths) do + push_details.modified_paths + end + end + def default_branch? ref == project.default_branch end @@ -660,6 +672,22 @@ module Ci Gitlab::DataBuilder::Pipeline.build(self) end + def push_details + strong_memoize(:push_details) do + Gitlab::Git::Push.new(project, before_sha, sha, push_ref) + end + end + + def push_ref + if branch? + Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s + elsif tag? + Gitlab::Git::TAG_REF_PREFIX + ref.to_s + else + raise ArgumentError, 'Invalid pipeline type!' + end + end + def latest_builds_status return 'failed' unless yaml_errors.blank? diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb new file mode 100644 index 00000000000..b61bf29e6ad --- /dev/null +++ b/app/models/concerns/diff_positionable_note.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true +module DiffPositionableNote + extend ActiveSupport::Concern + + included do + before_validation :set_original_position, on: :create + before_validation :update_position, on: :create, if: :on_text? + + serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize + serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize + serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize + end + + %i(original_position position change_position).each do |meth| + define_method "#{meth}=" do |new_position| + if new_position.is_a?(String) + new_position = JSON.parse(new_position) rescue nil + end + + if new_position.is_a?(Hash) + new_position = new_position.with_indifferent_access + new_position = Gitlab::Diff::Position.new(new_position) + end + + return if new_position == read_attribute(meth) + + super(new_position) + end + end + + def on_text? + position&.position_type == "text" + end + + def on_image? + position&.position_type == "image" + end + + def supported? + for_commit? || self.noteable.has_complete_diff_refs? + end + + def active?(diff_refs = nil) + return false unless supported? + return true if for_commit? + + diff_refs ||= noteable.diff_refs + + self.position.diff_refs == diff_refs + end + + def set_original_position + return unless position + + self.original_position = self.position.dup unless self.original_position&.complete? + end + + def update_position + return unless supported? + return if for_commit? + + return if active? + return unless position + + tracer = Gitlab::Diff::PositionTracer.new( + project: self.project, + old_diff_refs: self.position.diff_refs, + new_diff_refs: self.noteable.diff_refs, + paths: self.position.paths + ) + + result = tracer.trace(self.position) + return unless result + + if result[:outdated] + self.change_position = result[:position] + else + self.position = result[:position] + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5f65fceb7af..2aa52bbaeea 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -76,6 +76,7 @@ module Issuable scope :recent, -> { reorder(id: :desc) } scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_milestones, ->(ids) { where(milestone_id: ids) } + scope :any_milestone, -> { where('milestone_id IS NOT NULL') } scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } scope :opened, -> { with_state(:opened) } scope :only_opened, -> { with_state(:opened) } diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 047d353b4b5..95694377fe3 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -5,14 +5,11 @@ # A note of this type can be resolvable. class DiffNote < Note include NoteOnDiff + include DiffPositionableNote include Gitlab::Utils::StrongMemoize NOTEABLE_TYPES = %w(MergeRequest Commit).freeze - serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize - serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize - serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize - validates :original_position, presence: true validates :position, presence: true validates :line_code, presence: true, line_code: true, if: :on_text? @@ -21,8 +18,6 @@ class DiffNote < Note validate :verify_supported validate :diff_refs_match_commit, if: :for_commit? - before_validation :set_original_position, on: :create - before_validation :update_position, on: :create, if: :on_text? before_validation :set_line_code, if: :on_text? after_save :keep_around_commits after_commit :create_diff_file, on: :create @@ -31,31 +26,6 @@ class DiffNote < Note DiffDiscussion end - %i(original_position position change_position).each do |meth| - define_method "#{meth}=" do |new_position| - if new_position.is_a?(String) - new_position = JSON.parse(new_position) rescue nil - end - - if new_position.is_a?(Hash) - new_position = new_position.with_indifferent_access - new_position = Gitlab::Diff::Position.new(new_position) - end - - return if new_position == read_attribute(meth) - - super(new_position) - end - end - - def on_text? - position.position_type == "text" - end - - def on_image? - position.position_type == "image" - end - def create_diff_file return unless should_create_diff_file? @@ -87,15 +57,6 @@ class DiffNote < Note self.diff_file.line_code(self.diff_line) end - def active?(diff_refs = nil) - return false unless supported? - return true if for_commit? - - diff_refs ||= noteable.diff_refs - - self.position.diff_refs == diff_refs - end - def created_at_diff?(diff_refs) return false unless supported? return true if for_commit? @@ -141,37 +102,10 @@ class DiffNote < Note for_commit? || self.noteable.has_complete_diff_refs? end - def set_original_position - self.original_position = self.position.dup unless self.original_position&.complete? - end - def set_line_code self.line_code = self.position.line_code(self.project.repository) end - def update_position - return unless supported? - return if for_commit? - - return if active? - - tracer = Gitlab::Diff::PositionTracer.new( - project: self.project, - old_diff_refs: self.position.diff_refs, - new_diff_refs: self.noteable.diff_refs, - paths: self.position.paths - ) - - result = tracer.trace(self.position) - return unless result - - if result[:outdated] - self.change_position = result[:position] - else - self.position = result[:position] - end - end - def verify_supported return if supported? diff --git a/app/models/project.rb b/app/models/project.rb index 59f088156c7..dc2732cc6c2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -55,8 +55,8 @@ class Project < ActiveRecord::Base cache_markdown_field :description, pipeline: :description delegate :feature_available?, :builds_enabled?, :wiki_enabled?, - :merge_requests_enabled?, :issues_enabled?, to: :project_feature, - allow_nil: true + :merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?, + to: :project_feature, allow_nil: true delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage @@ -356,7 +356,7 @@ class Project < ActiveRecord::Base # "enabled" here means "not disabled". It includes private features! scope :with_feature_enabled, ->(feature) { access_level_attribute = ProjectFeature.access_level_attribute(feature) - with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] }) + with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] }) } # Picks a feature where the level is exactly that given. @@ -418,15 +418,15 @@ class Project < ActiveRecord::Base end end - # project features may be "disabled", "internal" or "enabled". If "internal", + # project features may be "disabled", "internal", "enabled" or "public". If "internal", # they are only available to team members. This scope returns projects where - # the feature is either enabled, or internal with permission for the user. + # the feature is either public, enabled, or internal with permission for the user. # # This method uses an optimised version of `with_feature_access_level` for # logged in users to more efficiently get private projects with the given # feature. def self.with_feature_available_for_user(feature, user) - visible = [nil, ProjectFeature::ENABLED] + visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] if user&.admin? with_feature_enabled(feature) diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 4a0324e8b5c..39f2b8fe0de 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -13,14 +13,16 @@ class ProjectFeature < ActiveRecord::Base # Disabled: not enabled for anyone # Private: enabled only for team members # Enabled: enabled for everyone able to access the project + # Public: enabled for everyone (only allowed for pages) # # Permission levels DISABLED = 0 PRIVATE = 10 ENABLED = 20 + PUBLIC = 30 - FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze + FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze class << self def access_level_attribute(feature) @@ -46,6 +48,7 @@ class ProjectFeature < ActiveRecord::Base validates :project, presence: true validate :repository_children_level + validate :allowed_access_levels default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :issues_access_level, value: ENABLED, allows_nil: false @@ -55,6 +58,9 @@ class ProjectFeature < ActiveRecord::Base default_value_for :repository_access_level, value: ENABLED, allows_nil: false def feature_available?(feature, user) + # This feature might not be behind a feature flag at all, so default to true + return false unless ::Feature.enabled?(feature, user, default_enabled: true) + get_permission(user, access_level(feature)) end @@ -78,6 +84,16 @@ class ProjectFeature < ActiveRecord::Base issues_access_level > DISABLED end + def pages_enabled? + pages_access_level > DISABLED + end + + def public_pages? + return true unless Gitlab.config.pages.access_control + + pages_access_level == PUBLIC || pages_access_level == ENABLED && project.public? + end + private # Validates builds and merge requests access level @@ -92,6 +108,17 @@ class ProjectFeature < ActiveRecord::Base %i(merge_requests_access_level builds_access_level).each(&validator) end + # Validates access level for other than pages cannot be PUBLIC + def allowed_access_levels + validator = lambda do |field| + level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend + not_allowed = level > ProjectFeature::ENABLED + self.errors.add(field, "cannot have public visibility level") if not_allowed + end + + (FEATURES - %i(pages)).each {|f| validator.call("#{f}_access_level")} + end + def get_permission(user, level) case level when DISABLED @@ -100,6 +127,8 @@ class ProjectFeature < ActiveRecord::Base user && (project.team.member?(user) || user.full_private_access?) when ENABLED true + when PUBLIC + true else true end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index f2c246cd969..a76a083bceb 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -110,6 +110,7 @@ class ProjectPolicy < BasePolicy snippets wiki builds + pages ] features.each do |f| @@ -167,6 +168,7 @@ class ProjectPolicy < BasePolicy enable :upload_file enable :read_cycle_analytics enable :award_emoji + enable :read_pages_content end # These abilities are not allowed to admins that are not members of the project, @@ -286,6 +288,8 @@ class ProjectPolicy < BasePolicy prevent(*create_read_update_admin_destroy(:merge_request)) end + rule { pages_disabled }.prevent :read_pages_content + rule { issues_disabled & merge_requests_disabled }.policy do prevent(*create_read_update_admin_destroy(:label)) prevent(*create_read_update_admin_destroy(:milestone)) @@ -345,6 +349,7 @@ class ProjectPolicy < BasePolicy enable :download_code enable :download_wiki_code enable :read_cycle_analytics + enable :read_pages_content # NOTE: may be overridden by IssuePolicy enable :read_issue diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index c85b1790e73..3d508a9a407 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class BuildDetailsEntity < JobEntity - include EnvironmentHelper - include RequestAwareEntity - include CiStatusHelper - expose :coverage, :erased_at, :duration expose :tag_list, as: :tags expose :has_trace?, as: :has_trace @@ -15,10 +11,6 @@ class BuildDetailsEntity < JobEntity expose :deployment_status, if: -> (*) { build.has_environment? } do expose :deployment_status, as: :status - expose :icon do |build| - ci_label_for_status(build.status) - end - expose :persisted_environment, as: :environment, with: EnvironmentEntity end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index aa5d8406d0f..28c3219b37b 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -57,10 +57,10 @@ module MergeRequests # Returns all origin and fork merge requests from `@project` satisfying passed arguments. # rubocop: disable CodeReuse/ActiveRecord def merge_requests_for(source_branch, mr_states: [:opened]) - MergeRequest + @project.source_of_merge_requests .with_state(mr_states) - .where(source_branch: source_branch, source_project_id: @project.id) - .preload(:source_project) # we don't need a #includes since we're just preloading for the #select + .where(source_branch: source_branch) + .preload(:source_project) # we don't need #includes since we're just preloading for the #select .select(&:source_project) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index bcdd752ddc4..d3e4f3def23 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -3,17 +3,16 @@ module MergeRequests class RefreshService < MergeRequests::BaseService def execute(oldrev, newrev, ref) - return true unless Gitlab::Git.branch_ref?(ref) + @push = Gitlab::Git::Push.new(@project, oldrev, newrev, ref) - do_execute(oldrev, newrev, ref) + return true unless @push.branch_push? + + refresh_merge_requests! end private - def do_execute(oldrev, newrev, ref) - @oldrev, @newrev = oldrev, newrev - @branch_name = Gitlab::Git.ref_name(ref) - + def refresh_merge_requests! Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits)) # Be sure to close outstanding MRs before reloading them to avoid generating an # empty diff during a manual merge @@ -25,7 +24,7 @@ module MergeRequests cache_merge_requests_closing_issues # Leave a system note if a branch was deleted/added - if branch_added? || branch_removed? + if @push.branch_added? || @push.branch_removed? comment_mr_branch_presence_changed end @@ -54,8 +53,10 @@ module MergeRequests # rubocop: disable CodeReuse/ActiveRecord def post_merge_manually_merged commit_ids = @commits.map(&:id) - merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a - merge_requests = merge_requests.select(&:diff_head_commit) + merge_requests = @project.merge_requests.opened + .preload(:latest_merge_request_diff) + .where(target_branch: @push.branch_name).to_a + .select(&:diff_head_commit) merge_requests = merge_requests.select do |merge_request| commit_ids.include?(merge_request.diff_head_sha) && @@ -70,24 +71,20 @@ module MergeRequests end # rubocop: enable CodeReuse/ActiveRecord - def force_push? - Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev) - end - # Refresh merge request diff if we push to source or target branch of merge request # Note: we should update merge requests from forks too # rubocop: disable CodeReuse/ActiveRecord def reload_merge_requests merge_requests = @project.merge_requests.opened - .by_source_or_target_branch(@branch_name).to_a + .by_source_or_target_branch(@push.branch_name).to_a # Fork merge requests merge_requests += MergeRequest.opened - .where(source_branch: @branch_name, source_project: @project) + .where(source_branch: @push.branch_name, source_project: @project) .where.not(target_project: @project).to_a filter_merge_requests(merge_requests).each do |merge_request| - if merge_request.source_branch == @branch_name || force_push? + if merge_request.source_branch == @push.branch_name || @push.force_push? merge_request.reload_diff(current_user) else mr_commit_ids = merge_request.commit_shas @@ -117,7 +114,7 @@ module MergeRequests end def find_new_commits - if branch_added? + if @push.branch_added? @commits = [] merge_request = merge_requests_for_source_branch.first @@ -126,28 +123,28 @@ module MergeRequests begin # Since any number of commits could have been made to the restored branch, # find the common root to see what has been added. - common_ref = @project.repository.merge_base(merge_request.diff_head_sha, @newrev) + common_ref = @project.repository.merge_base(merge_request.diff_head_sha, @push.newrev) # If the a commit no longer exists in this repo, gitlab_git throws # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 - @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref + @commits = @project.repository.commits_between(common_ref, @push.newrev) if common_ref rescue end - elsif branch_removed? + elsif @push.branch_removed? # No commits for a deleted branch. @commits = [] else - @commits = @project.repository.commits_between(@oldrev, @newrev) + @commits = @project.repository.commits_between(@push.oldrev, @push.newrev) end end # Add comment about branches being deleted or added to merge requests def comment_mr_branch_presence_changed - presence = branch_added? ? :add : :delete + presence = @push.branch_added? ? :add : :delete merge_requests_for_source_branch.each do |merge_request| SystemNoteService.change_branch_presence( merge_request, merge_request.project, @current_user, - :source, @branch_name, presence) + :source, @push.branch_name, presence) end end @@ -164,7 +161,7 @@ module MergeRequests SystemNoteService.add_commits(merge_request, merge_request.project, @current_user, new_commits, - existing_commits, @oldrev) + existing_commits, @push.oldrev) notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits) end @@ -195,7 +192,7 @@ module MergeRequests # Call merge request webhook with update branches def execute_mr_web_hooks merge_requests_for_source_branch.each do |merge_request| - execute_hooks(merge_request, 'update', old_rev: @oldrev) + execute_hooks(merge_request, 'update', old_rev: @push.oldrev) end end @@ -203,7 +200,7 @@ module MergeRequests # `MergeRequestsClosingIssues` model (as a performance optimization). # rubocop: disable CodeReuse/ActiveRecord def cache_merge_requests_closing_issues - @project.merge_requests.where(source_branch: @branch_name).each do |merge_request| + @project.merge_requests.where(source_branch: @push.branch_name).each do |merge_request| merge_request.cache_merge_request_closes_issues!(@current_user) end end @@ -215,15 +212,7 @@ module MergeRequests def merge_requests_for_source_branch(reload: false) @source_merge_requests = nil if reload - @source_merge_requests ||= merge_requests_for(@branch_name) - end - - def branch_added? - Gitlab::Git.blank_ref?(@oldrev) - end - - def branch_removed? - Gitlab::Git.blank_ref?(@newrev) + @source_merge_requests ||= merge_requests_for(@push.branch_name) end end end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 7b747171d9c..61f6402a810 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -29,6 +29,10 @@ module Projects QuickActions::InterpretService.new(project, current_user).available_commands(noteable) end + def snippets + SnippetsFinder.new(current_user, project: project).execute.select([:id, :title]) + end + def labels_as_hash(target) super(target, project_id: project.id, include_ancestor_groups: true) end diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb index efbd4c7b323..abf40b3ad7a 100644 --- a/app/services/projects/update_pages_configuration_service.rb +++ b/app/services/projects/update_pages_configuration_service.rb @@ -21,7 +21,9 @@ module Projects def pages_config { domains: pages_domains_config, - https_only: project.pages_https_only? + https_only: project.pages_https_only?, + id: project.project_id, + access_control: !project.public_pages? } end @@ -31,7 +33,9 @@ module Projects domain: domain.domain, certificate: domain.certificate, key: domain.key, - https_only: project.pages_https_only? && domain.https? + https_only: project.pages_https_only? && domain.https?, + id: project.project_id, + access_control: !project.public_pages? } end end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index d6d9bacf232..f25a4e30938 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -72,7 +72,11 @@ module Projects system_hook_service.execute_hooks_for(project, :update) end - update_pages_config if changing_pages_https_only? + update_pages_config if changing_pages_related_config? + end + + def changing_pages_related_config? + changing_pages_https_only? || changing_pages_access_level? end def update_failed! @@ -102,6 +106,10 @@ module Projects params.dig(:project_feature_attributes, :wiki_access_level).to_i > ProjectFeature::DISABLED end + def changing_pages_access_level? + params.dig(:project_feature_attributes, :pages_access_level) + end + def ensure_wiki_exists ProjectWiki.new(project, project.owner).wiki rescue ProjectWiki::CouldNotCreateWikiError diff --git a/changelogs/unreleased/22188-drop-down-filter-for-project-snippets.yml b/changelogs/unreleased/22188-drop-down-filter-for-project-snippets.yml new file mode 100644 index 00000000000..e24c55e3bad --- /dev/null +++ b/changelogs/unreleased/22188-drop-down-filter-for-project-snippets.yml @@ -0,0 +1,5 @@ +--- +title: Add autocomplete drop down filter for project snippets +merge_request: 21458 +author: Fabian Schneider +type: added diff --git a/changelogs/unreleased/50246-can-t-sort-group-issues-by-popularity-when-searching.yml b/changelogs/unreleased/50246-can-t-sort-group-issues-by-popularity-when-searching.yml new file mode 100644 index 00000000000..cc7a79d25e5 --- /dev/null +++ b/changelogs/unreleased/50246-can-t-sort-group-issues-by-popularity-when-searching.yml @@ -0,0 +1,6 @@ +--- +title: Fix sorting by priority or popularity on group issues page, when also searching + issue content +merge_request: 21521 +author: +type: fixed diff --git a/changelogs/unreleased/51748-filter-any-milestone-via-api.yml b/changelogs/unreleased/51748-filter-any-milestone-via-api.yml new file mode 100644 index 00000000000..30304e5a4ac --- /dev/null +++ b/changelogs/unreleased/51748-filter-any-milestone-via-api.yml @@ -0,0 +1,5 @@ +--- +title: Allows to filter issues by Any milestone in the API +merge_request: 22080 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/auth.yml b/changelogs/unreleased/auth.yml new file mode 100644 index 00000000000..cd4bbf0059e --- /dev/null +++ b/changelogs/unreleased/auth.yml @@ -0,0 +1,5 @@ +--- +title: Add access control to GitLab pages and make it possible to enable/disable it in project settings +merge_request: 18589 +author: Tuomo Ala-Vannesluoma +type: added diff --git a/changelogs/unreleased/feature-gb-pipeline-only-except-with-modified-paths.yml b/changelogs/unreleased/feature-gb-pipeline-only-except-with-modified-paths.yml new file mode 100644 index 00000000000..62676cdad62 --- /dev/null +++ b/changelogs/unreleased/feature-gb-pipeline-only-except-with-modified-paths.yml @@ -0,0 +1,5 @@ +--- +title: Add support for pipeline only/except policy for modified paths +merge_request: 21981 +author: +type: added diff --git a/changelogs/unreleased/features-unauth-access-ssh-keys.yml b/changelogs/unreleased/features-unauth-access-ssh-keys.yml new file mode 100644 index 00000000000..bae2bcfaabd --- /dev/null +++ b/changelogs/unreleased/features-unauth-access-ssh-keys.yml @@ -0,0 +1,5 @@ +--- +title: Enable unauthenticated access to public SSH keys via the API +merge_request: 20118 +author: Ronald Claveau +type: changed diff --git a/changelogs/unreleased/mao-48221-issues_show_sql_count.yml b/changelogs/unreleased/mao-48221-issues_show_sql_count.yml new file mode 100644 index 00000000000..d634d15946e --- /dev/null +++ b/changelogs/unreleased/mao-48221-issues_show_sql_count.yml @@ -0,0 +1,5 @@ +--- +title: Banzai label ref finder - minimize SQL calls by sharing context more aggresively +merge_request: 22070 +author: +type: performance diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 67337f4b82f..749cdd0f869 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -210,6 +210,7 @@ production: &base ## GitLab Pages pages: enabled: false + access_control: false # The location where pages are stored (default: shared/pages). # path: shared/pages diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 0caa4962128..bd02b85c7ce 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -200,6 +200,7 @@ Settings.registry['path'] = Settings.absolute(Settings.registry['path # Settings['pages'] ||= Settingslogic.new({}) Settings.pages['enabled'] = false if Settings.pages['enabled'].nil? +Settings.pages['access_control'] = false if Settings.pages['access_control'].nil? Settings.pages['path'] = Settings.absolute(Settings.pages['path'] || File.join(Settings.shared['path'], "pages")) Settings.pages['https'] = false if Settings.pages['https'].nil? Settings.pages['host'] ||= "example.com" diff --git a/config/routes/project.rb b/config/routes/project.rb index 356a509da75..9cbd5b644f6 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -32,6 +32,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get 'labels' get 'milestones' get 'commands' + get 'snippets' end end diff --git a/db/migrate/20180423204600_add_pages_access_level_to_project_feature.rb b/db/migrate/20180423204600_add_pages_access_level_to_project_feature.rb new file mode 100644 index 00000000000..1d2f8cf9c76 --- /dev/null +++ b/db/migrate/20180423204600_add_pages_access_level_to_project_feature.rb @@ -0,0 +1,16 @@ +class AddPagesAccessLevelToProjectFeature < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default(:project_features, :pages_access_level, :integer, default: ProjectFeature::PUBLIC, allow_null: false) + + change_column_default(:project_features, :pages_access_level, ProjectFeature::ENABLED) + end + + def down + remove_column :project_features, :pages_access_level + end +end diff --git a/db/schema.rb b/db/schema.rb index 82f50e0ecd2..773bfb96b93 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1580,6 +1580,7 @@ ActiveRecord::Schema.define(version: 20180924201039) do t.datetime "created_at" t.datetime "updated_at" t.integer "repository_access_level", default: 20, null: false + t.integer "pages_access_level", default: 20, null: false end add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", unique: true, using: :btree diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 3af0a5759a7..2952a98626a 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -92,9 +92,8 @@ where `example.io` is the domain under which GitLab Pages will be served and `192.0.2.1` is the IPv4 address of your GitLab instance and `2001::1` is the IPv6 address. If you don't have IPv6, you can omit the AAAA record. -> **Note:** -You should not use the GitLab domain to serve user pages. For more information -see the [security section](#security). +NOTE: **Note:** +You should not use the GitLab domain to serve user pages. For more information see the [security section](#security). [wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record @@ -107,12 +106,13 @@ since that is needed in all configurations. ### Wildcard domains -> **Requirements:** -> - [Wildcard DNS setup](#dns-configuration) -> -> --- -> -> URL scheme: `http://page.example.io` +**Requirements:** + +- [Wildcard DNS setup](#dns-configuration) + +--- + +URL scheme: `http://page.example.io` This is the minimum setup that you can use Pages with. It is the base for all other setups as described below. Nginx will proxy all requests to the daemon. @@ -126,18 +126,18 @@ The Pages daemon doesn't listen to the outside world. 1. [Reconfigure GitLab][reconfigure] - Watch the [video tutorial][video-admin] for this configuration. ### Wildcard domains with TLS support -> **Requirements:** -> - [Wildcard DNS setup](#dns-configuration) -> - Wildcard TLS certificate -> -> --- -> -> URL scheme: `https://page.example.io` +**Requirements:** + +- [Wildcard DNS setup](#dns-configuration) +- Wildcard TLS certificate + +--- + +URL scheme: `https://page.example.io` Nginx will proxy all requests to the daemon. Pages daemon doesn't listen to the outside world. @@ -168,13 +168,14 @@ you have IPv6 as well as IPv4 addresses, you can use them both. ### Custom domains -> **Requirements:** -> - [Wildcard DNS setup](#dns-configuration) -> - Secondary IP -> -> --- -> -> URL scheme: `http://page.example.io` and `http://domain.com` +**Requirements:** + +- [Wildcard DNS setup](#dns-configuration) +- Secondary IP + +--- + +URL scheme: `http://page.example.io` and `http://domain.com` In that case, the Pages daemon is running, Nginx still proxies requests to the daemon but the daemon is also able to receive requests from the outside @@ -197,14 +198,15 @@ world. Custom domains are supported, but no TLS. ### Custom domains with TLS support -> **Requirements:** -> - [Wildcard DNS setup](#dns-configuration) -> - Wildcard TLS certificate -> - Secondary IP -> -> --- -> -> URL scheme: `https://page.example.io` and `https://domain.com` +**Requirements:** + +- [Wildcard DNS setup](#dns-configuration) +- Wildcard TLS certificate +- Secondary IP + +--- + +URL scheme: `https://page.example.io` and `https://domain.com` In that case, the Pages daemon is running, Nginx still proxies requests to the daemon but the daemon is also able to receive requests from the outside @@ -320,12 +322,12 @@ latest previous version. --- -**GitLab 8.17 ([documentation][8-17-docs])** +**GitLab 8.17 ([documentation](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/doc/administration/pages/index.md))** - GitLab Pages were ported to Community Edition in GitLab 8.17. - Documentation was refactored to be more modular and easy to follow. -**GitLab 8.5 ([documentation][8-5-docs])** +**GitLab 8.5 ([documentation](https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md))** - In GitLab 8.5 we introduced the [gitlab-pages][] daemon which is now the recommended way to set up GitLab Pages. @@ -334,13 +336,10 @@ latest previous version. - Custom CNAME and TLS certificates support. - Documentation was moved to one place. -**GitLab 8.3 ([documentation][8-3-docs])** +**GitLab 8.3 ([documentation](https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md))** - GitLab Pages feature was introduced. -[8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md -[8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md -[8-17-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable-ce/doc/administration/pages/index.md [backup]: ../../raketasks/backup_restore.md [ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 diff --git a/doc/api/issues.md b/doc/api/issues.md index f4c0f4ea65b..cc1d6834a20 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -37,7 +37,7 @@ GET /issues?my_reaction_emoji=star | ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | -| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone | +| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone. `Any+Milestone` lists all issues that have an assigned milestone | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | | `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | diff --git a/doc/api/users.md b/doc/api/users.md index 3b41e0f7ec6..07f03f9c827 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -558,7 +558,7 @@ Parameters: ## List SSH keys for user -Get a list of a specified user's SSH keys. Available only for admin +Get a list of a specified user's SSH keys. ``` GET /users/:id/keys diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index e38628b288b..15dde36cca8 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -387,6 +387,8 @@ except master. > `refs` and `kubernetes` policies introduced in GitLab 10.0 > > `variables` policy introduced in 10.7 +> +> `changes` policy [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/19232) in 11.4 CAUTION: **Warning:** This an _alpha_ feature, and it it subject to change at any time without @@ -398,10 +400,15 @@ policy configuration. GitLab now supports both, simple and complex strategies, so it is possible to use an array and a hash configuration scheme. -Three keys are now available: `refs`, `kubernetes` and `variables`. +Four keys are now available: `refs`, `kubernetes` and `variables` and `changes`. + +### `refs` and `kubernetes` + Refs strategy equals to simplified only/except configuration, whereas kubernetes strategy accepts only `active` keyword. +### `variables` + `variables` keyword is used to define variables expressions. In other words you can use predefined variables / project / group or environment-scoped variables to define an expression GitLab is going to @@ -445,6 +452,46 @@ end-to-end: Learn more about variables expressions on [a separate page][variables-expressions]. +### `changes` + +Using `changes` keyword with `only` or `except` makes it possible to define if +a job should be created based on files modified by a git push event. + +For example: + +```yaml +docker build: + script: docker build -t my-image:$CI_COMMIT_REF_SLUG . + only: + changes: + - Dockerfile + - docker/scripts/* +``` + +In the scenario above, if you are pushing multiple commits to GitLab to an +existing branch, GitLab creates and triggers `docker build` job, provided that +one of the commits contains changes to either: + +- The `Dockerfile` file. +- Any of the files inside `docker/scripts/` directory. + +CAUTION: **Warning:** +There are some caveats when using this feature with new branches and tags. See +the section below. + +#### Using `changes` with new branches and tags + +If you are pushing a **new** branch or a **new** tag to GitLab, the policy +always evaluates to true and GitLab will create a job. This feature is not +connected with merge requests yet, and because GitLab is creating pipelines +before an user can create a merge request we don't know a target branch at +this point. + +Without a target branch, it is not possible to know what the common ancestor is, +thus we always create a job in that case. This feature works best for stable +branches like `master` because in that case GitLab uses the previous commit +that is present in a branch to compare against the latest SHA that was pushed. + ## `tags` `tags` is used to select specific Runners from the list of all Runners that are diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index fed29d37b26..2f06677bfec 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -60,7 +60,7 @@ people. The current team labels are: -- ~Configuration +- ~Configure - ~"CI/CD" - ~Create - ~Distribution diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 8369cff2386..4a1ca41ab97 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -110,6 +110,7 @@ which visibility level you select on project settings. - Disabled: disabled for everyone - Only team members: only team members will see even if your project is public or internal - Everyone with access: everyone can see depending on your project visibility level +- Everyone: enabled for everyone (only available for GitLab Pages) ### Protected branches @@ -242,6 +243,7 @@ which visibility level you select on project settings. - Disabled: disabled for everyone - Only team members: only team members will see even if your project is public or internal - Everyone with access: everyone can see depending on your project visibility level +- Everyone: enabled for everyone (only available for GitLab Pages) ## GitLab CI/CD permissions diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 00bad49ebdc..ae2d327e45b 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -287,6 +287,12 @@ module API present_projects forks end + desc 'Check pages access of this project' + get ':id/pages_access' do + authorize! :read_pages_content, user_project unless user_project.public_pages? + status 200 + end + desc 'Update an existing project' do success Entities::Project end diff --git a/lib/api/users.rb b/lib/api/users.rb index 11a7f4ef64d..501c5cf1df3 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -256,7 +256,7 @@ module API end # rubocop: enable CodeReuse/ActiveRecord - desc 'Get the SSH keys of a specified user. Available only for admins.' do + desc 'Get the SSH keys of a specified user.' do success Entities::SSHKey end params do @@ -265,10 +265,8 @@ module API end # rubocop: disable CodeReuse/ActiveRecord get ':id/keys' do - authenticated_as_admin! - user = User.find_by(id: params[:id]) - not_found!('User') unless user + not_found!('User') unless user && can?(current_user, :read_user, user) present paginate(user.keys), with: Entities::SSHKey end diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb index 3f1e95d4cc0..43f913a8859 100644 --- a/lib/banzai/cross_project_reference.rb +++ b/lib/banzai/cross_project_reference.rb @@ -13,6 +13,7 @@ module Banzai # Returns a Project, or nil if the reference can't be found def parent_from_ref(ref) return context[:project] || context[:group] unless ref + return context[:project] if context[:project]&.full_path == ref Project.find_by_full_path(ref) end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index b92e9e55bb9..04ec38209c7 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -48,7 +48,7 @@ module Banzai include_ancestor_groups: true, only_group_labels: true } else - { project_id: parent.id, + { project: parent, include_ancestor_groups: true } end diff --git a/lib/gitlab/ci/build/policy/changes.rb b/lib/gitlab/ci/build/policy/changes.rb new file mode 100644 index 00000000000..7bf51519752 --- /dev/null +++ b/lib/gitlab/ci/build/policy/changes.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + module Policy + class Changes < Policy::Specification + def initialize(globs) + @globs = Array(globs) + end + + def satisfied_by?(pipeline, seed) + return true unless pipeline.branch_updated? + + pipeline.modified_paths.any? do |path| + @globs.any? do |glob| + File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH) + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb index 09e8e52b60f..c92562f8c85 100644 --- a/lib/gitlab/ci/config/entry/policy.rb +++ b/lib/gitlab/ci/config/entry/policy.rb @@ -25,17 +25,19 @@ module Gitlab include Entry::Validatable include Entry::Attributable - attributes :refs, :kubernetes, :variables + ALLOWED_KEYS = %i[refs kubernetes variables changes].freeze + attributes :refs, :kubernetes, :variables, :changes validations do validates :config, presence: true - validates :config, allowed_keys: %i[refs kubernetes variables] + validates :config, allowed_keys: ALLOWED_KEYS validate :variables_expressions_syntax with_options allow_nil: true do validates :refs, array_of_strings_or_regexps: true validates :kubernetes, allowed_values: %w[active] validates :variables, array_of_strings: true + validates :changes, array_of_strings: true end def variables_expressions_syntax diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index a96595b33a5..ed4ec7e6385 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -689,9 +689,6 @@ rollout 100%: helm version --client tiller -version - helm init --client-only - helm plugin install https://github.com/adamreese/helm-local - 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 @@ -800,9 +797,9 @@ rollout 100%: function initialize_tiller() { echo "Checking Tiller..." - helm local start - helm local status export HELM_HOST=":44134" + tiller -listen ${HELM_HOST} -alsologtostderr > /dev/null 2>&1 & + echo "Tiller is listening on ${HELM_HOST}" if ! helm version --debug; then echo "Failed to init Tiller." diff --git a/lib/gitlab/git/diff_stats_collection.rb b/lib/gitlab/git/diff_stats_collection.rb index d4033f56387..998c41497a2 100644 --- a/lib/gitlab/git/diff_stats_collection.rb +++ b/lib/gitlab/git/diff_stats_collection.rb @@ -18,6 +18,10 @@ module Gitlab indexed_by_path[path] end + def paths + @collection.map(&:path) + end + private def indexed_by_path diff --git a/lib/gitlab/git/push.rb b/lib/gitlab/git/push.rb new file mode 100644 index 00000000000..7c1309721fd --- /dev/null +++ b/lib/gitlab/git/push.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Gitlab + module Git + class Push + include Gitlab::Utils::StrongMemoize + + attr_reader :oldrev, :newrev + + def initialize(project, oldrev, newrev, ref) + @project = project + @oldrev = oldrev.presence || Gitlab::Git::BLANK_SHA + @newrev = newrev.presence || Gitlab::Git::BLANK_SHA + @ref = ref + end + + def branch_name + strong_memoize(:branch_name) do + Gitlab::Git.branch_name(@ref) + end + end + + def branch_added? + Gitlab::Git.blank_ref?(@oldrev) + end + + def branch_removed? + Gitlab::Git.blank_ref?(@newrev) + end + + def branch_updated? + branch_push? && !branch_added? && !branch_removed? + end + + def force_push? + Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev) + end + + def branch_push? + strong_memoize(:branch_push) do + Gitlab::Git.branch_ref?(@ref) + end + end + + def modified_paths + unless branch_updated? + raise ArgumentError, 'Unable to calculate modified paths!' + end + + strong_memoize(:modified_paths) do + @project.repository.diff_stats(@oldrev, @newrev).paths + end + end + end + end +end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 500aabcbbb8..4ec87f6a3e7 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -220,7 +220,7 @@ module Gitlab result end - SERVER_FEATURE_FLAGS = %w[gogit_findcommit git_v2].freeze + SERVER_FEATURE_FLAGS = %w[gogit_findcommit].freeze def self.server_feature_flags SERVER_FEATURE_FLAGS.map do |f| @@ -257,6 +257,9 @@ module QA autoload :Dropzone, 'qa/page/component/dropzone' autoload :GroupsFilter, 'qa/page/component/groups_filter' autoload :Select2, 'qa/page/component/select2' + module Issuable + autoload :Common, 'qa/page/component/issuable/common' + end end end diff --git a/qa/qa/page/component/issuable/common.rb b/qa/qa/page/component/issuable/common.rb new file mode 100644 index 00000000000..cfd8ac1e7c8 --- /dev/null +++ b/qa/qa/page/component/issuable/common.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module Issuable + module Common + def self.included(base) + base.view 'app/assets/javascripts/issue_show/components/title.vue' do + element :edit_button + end + + base.view 'app/assets/javascripts/issue_show/components/fields/title.vue' do + element :title_input + end + + base.view 'app/assets/javascripts/issue_show/components/fields/description.vue' do + element :description_textarea + end + + base.view 'app/assets/javascripts/issue_show/components/edit_actions.vue' do + element :save_button + element :delete_button + end + + base.view 'app/assets/javascripts/issue_show/components/edit_actions.vue' do + element :save_button + element :delete_button + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 587a02163b9..9a738f56202 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -5,6 +5,8 @@ module QA module Project module Issue class Show < Page::Base + include Page::Component::Issuable::Common + view 'app/views/projects/issues/show.html.haml' do element :issue_details, '.issue-details' element :title, '.title' diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 65d6cd1a295..a099cdafa58 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -202,8 +202,8 @@ describe GroupsController do end describe 'GET #issues' do - let(:issue_1) { create(:issue, project: project) } - let(:issue_2) { create(:issue, project: project) } + let(:issue_1) { create(:issue, project: project, title: 'foo') } + let(:issue_2) { create(:issue, project: project, title: 'bar') } before do create_list(:award_emoji, 3, awardable: issue_2) @@ -224,6 +224,31 @@ describe GroupsController do expect(assigns(:issues)).to eq [issue_2, issue_1] end end + + context 'searching' do + # Remove as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/52271 + before do + stub_feature_flags(use_cte_for_group_issues_search: false) + end + + it 'works with popularity sort' do + get :issues, id: group.to_param, search: 'foo', sort: 'popularity' + + expect(assigns(:issues)).to eq([issue_1]) + end + + it 'works with priority sort' do + get :issues, id: group.to_param, search: 'foo', sort: 'priority' + + expect(assigns(:issues)).to eq([issue_1]) + end + + it 'works with label priority sort' do + get :issues, id: group.to_param, search: 'foo', sort: 'label_priority' + + expect(assigns(:issues)).to eq([issue_1]) + end + end end describe 'GET #merge_requests' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 5b347b1109a..44419ed6a2a 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -637,6 +637,18 @@ describe Projects::IssuesController do project_id: project, id: id end + + it 'avoids (most) N+1s loading labels' do + label = create(:label, project: project).to_reference + labels = create_list(:label, 10, project: project).map(&:to_reference) + issue = create(:issue, project: project, description: 'Test issue') + + control_count = ActiveRecord::QueryRecorder.new { issue.update(description: [issue.description, label].join(' ')) }.count + + # Follow-up to get rid of this `2 * label.count` requirement: https://gitlab.com/gitlab-org/gitlab-ce/issues/52230 + expect { issue.update(description: [issue.description, labels].join(' ')) } + .not_to exceed_query_limit(control_count + 2 * labels.count) + end end describe 'GET #realtime_changes' do diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 5b09e4a082c..383d6c1a2a9 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -225,7 +225,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to match_schema('job/job_details') expect(json_response['deployment_status']["status"]).to eq 'creating' - expect(json_response['deployment_status']["icon"]).to eq 'passed' expect(json_response['deployment_status']["environment"]).not_to be_nil end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 80801eb1082..e4823a5adf1 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -1,6 +1,8 @@ require_relative '../support/helpers/test_env' FactoryBot.define do + PAGES_ACCESS_LEVEL_SCHEMA_VERSION = 20180423204600 + # Project without repository # # Project does not have bare repository. @@ -23,6 +25,7 @@ FactoryBot.define do issues_access_level ProjectFeature::ENABLED merge_requests_access_level ProjectFeature::ENABLED repository_access_level ProjectFeature::ENABLED + pages_access_level ProjectFeature::ENABLED # we can't assign the delegated `#ci_cd_settings` attributes directly, as the # `#ci_cd_settings` relation needs to be created first @@ -34,13 +37,20 @@ FactoryBot.define do builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min - project.project_feature.update( + hash = { wiki_access_level: evaluator.wiki_access_level, builds_access_level: builds_access_level, snippets_access_level: evaluator.snippets_access_level, issues_access_level: evaluator.issues_access_level, merge_requests_access_level: merge_requests_access_level, - repository_access_level: evaluator.repository_access_level) + repository_access_level: evaluator.repository_access_level + } + + if ActiveRecord::Migrator.current_version >= PAGES_ACCESS_LEVEL_SCHEMA_VERSION + hash.store("pages_access_level", evaluator.pages_access_level) + end + + project.project_feature.update(hash) # Normally the class Projects::CreateService is used for creating # projects, and this class takes care of making sure the owner and current @@ -244,6 +254,10 @@ FactoryBot.define do trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED } trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED } trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE } + trait(:pages_public) { pages_access_level ProjectFeature::PUBLIC } + trait(:pages_enabled) { pages_access_level ProjectFeature::ENABLED } + trait(:pages_disabled) { pages_access_level ProjectFeature::DISABLED } + trait(:pages_private) { pages_access_level ProjectFeature::PRIVATE } trait :auto_devops do association :auto_devops, factory: :project_auto_devops diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 98e37d8011a..08bf9bc7243 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -5,6 +5,7 @@ describe 'GFM autocomplete', :js do let(:project) { create(:project) } let(:label) { create(:label, project: project, title: 'special+') } let(:issue) { create(:issue, project: project) } + let!(:project_snippet) { create(:project_snippet, project: project, title: 'code snippet') } before do project.add_maintainer(user) @@ -301,6 +302,16 @@ describe 'GFM autocomplete', :js do end end + it 'shows project snippets' do + page.within '.timeline-content-form' do + find('#note-body').native.send_keys('$') + end + + page.within '.atwho-container' do + expect(page).to have_content(project_snippet.title) + end + end + private def expect_to_wrap(should_wrap, item, note, value) diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index d78451112ec..0689c843104 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -125,6 +125,14 @@ describe IssuesFinder do end end + context 'filtering by any milestone' do + let(:params) { { milestone_title: Milestone::Any.title } } + + it 'returns issues with any assigned milestone' do + expect(issues).to contain_exactly(issue1) + end + end + context 'filtering by upcoming milestone' do let(:params) { { milestone_title: Milestone::Upcoming.name } } diff --git a/spec/fixtures/api/schemas/job/deployment_status.json b/spec/fixtures/api/schemas/job/deployment_status.json index a90b8b35654..83b1899fdf3 100644 --- a/spec/fixtures/api/schemas/job/deployment_status.json +++ b/spec/fixtures/api/schemas/job/deployment_status.json @@ -2,7 +2,6 @@ "type": "object", "required": [ "status", - "icon", "environment" ], "properties": { @@ -20,7 +19,6 @@ { "type": "null" } ] }, - "icon": { "type": "string" }, "environment": { "$ref": "../environment.json" } }, "additionalProperties": false diff --git a/spec/graphql/types/permission_types/project_spec.rb b/spec/graphql/types/permission_types/project_spec.rb index 89eecef096e..927153adc5b 100644 --- a/spec/graphql/types/permission_types/project_spec.rb +++ b/spec/graphql/types/permission_types/project_spec.rb @@ -10,7 +10,7 @@ describe Types::PermissionTypes::Project do :read_commit_status, :request_access, :create_pipeline, :create_pipeline_schedule, :create_merge_request_from, :create_wiki, :push_code, :create_deployment, :push_to_delete_protected_branch, :admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label, - :update_wiki, :destroy_wiki, :create_pages, :destroy_pages + :update_wiki, :destroy_wiki, :create_pages, :destroy_pages, :read_pages_content ] expect(described_class).to have_graphql_fields(expected_permissions) diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 1238cfbd1e7..4135f31e051 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -174,9 +174,7 @@ describe ApplicationHelper do it 'returns paths for autocomplete_sources_controller' do sources = helper.autocomplete_data_sources(project, noteable_type) - - expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands]) - + expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets]) sources.keys.each do |key| expect(sources[key]).not_to be_nil end diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js index 4f9cacf2724..b57c4943c01 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js +++ b/spec/javascripts/gfm_auto_complete_spec.js @@ -103,7 +103,7 @@ describe('GfmAutoComplete', function () { gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext) ); - const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%']; + const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%', '$']; const otherFlags = ['/', ':']; const flags = flagsUseDefaultMatcher.concat(otherFlags); diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb index aadfe7637dd..ba995e16be7 100644 --- a/spec/lib/banzai/cross_project_reference_spec.rb +++ b/spec/lib/banzai/cross_project_reference_spec.rb @@ -1,16 +1,21 @@ require 'spec_helper' describe Banzai::CrossProjectReference do - include described_class + let(:including_class) { Class.new.include(described_class).new } + + before do + allow(including_class).to receive(:context).and_return({}) + allow(including_class).to receive(:parent_from_ref).and_call_original + end describe '#parent_from_ref' do context 'when no project was referenced' do it 'returns the project from context' do project = double - allow(self).to receive(:context).and_return({ project: project }) + allow(including_class).to receive(:context).and_return({ project: project }) - expect(parent_from_ref(nil)).to eq project + expect(including_class.parent_from_ref(nil)).to eq project end end @@ -18,15 +23,15 @@ describe Banzai::CrossProjectReference do it 'returns the group from context' do group = double - allow(self).to receive(:context).and_return({ group: group }) + allow(including_class).to receive(:context).and_return({ group: group }) - expect(parent_from_ref(nil)).to eq group + expect(including_class.parent_from_ref(nil)).to eq group end end context 'when referenced project does not exist' do it 'returns nil' do - expect(parent_from_ref('invalid/reference')).to be_nil + expect(including_class.parent_from_ref('invalid/reference')).to be_nil end end @@ -37,7 +42,7 @@ describe Banzai::CrossProjectReference do expect(Project).to receive(:find_by_full_path) .with('cross/reference').and_return(project2) - expect(parent_from_ref('cross/reference')).to eq project2 + expect(including_class.parent_from_ref('cross/reference')).to eq project2 end end end diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index e1af5a15371..cbff2fdab14 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -60,6 +60,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter do exp = act = "See #{commit1.id.reverse}...#{commit2.id}" allow(project.repository).to receive(:commit).with(commit1.id.reverse) + allow(project.repository).to receive(:commit).with(commit2.id) expect(reference_filter(act).to_html).to eq exp end diff --git a/spec/lib/gitlab/ci/build/policy/changes_spec.rb b/spec/lib/gitlab/ci/build/policy/changes_spec.rb new file mode 100644 index 00000000000..ab401108c84 --- /dev/null +++ b/spec/lib/gitlab/ci/build/policy/changes_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Policy::Changes do + set(:project) { create(:project) } + + describe '#satisfied_by?' do + describe 'paths matching matching' do + let(:pipeline) do + build(:ci_empty_pipeline, project: project, + ref: 'master', + source: :push, + sha: '1234abcd', + before_sha: '0123aabb') + end + + let(:ci_build) do + build(:ci_build, pipeline: pipeline, project: project, ref: 'master') + end + + let(:seed) { double('build seed', to_resource: ci_build) } + + before do + allow(pipeline).to receive(:modified_paths) do + %w[some/modified/ruby/file.rb some/other_file.txt some/.dir/file] + end + end + + it 'is satisfied by matching literal path' do + policy = described_class.new(%w[some/other_file.txt]) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + + it 'is satisfied by matching simple pattern' do + policy = described_class.new(%w[some/*.txt]) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + + it 'is satisfied by matching recusive pattern' do + policy = described_class.new(%w[some/**/*.rb]) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + + it 'is satisfied by matching a pattern with a dot' do + policy = described_class.new(%w[some/*/file]) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + + it 'is not satisfied when pattern does not match path' do + policy = described_class.new(%w[some/*.rb]) + + expect(policy).not_to be_satisfied_by(pipeline, seed) + end + + it 'is not satisfied when pattern does not match' do + policy = described_class.new(%w[invalid/*.md]) + + expect(policy).not_to be_satisfied_by(pipeline, seed) + end + + context 'when pipelines does not run for a branch update' do + before do + pipeline.before_sha = Gitlab::Git::BLANK_SHA + end + + it 'is always satisfied' do + policy = described_class.new(%w[invalid/*]) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + end + end + + describe 'gitaly integration' do + set(:project) { create(:project, :repository) } + + let(:pipeline) do + create(:ci_empty_pipeline, project: project, + ref: 'master', + source: :push, + sha: '498214d', + before_sha: '281d3a7') + end + + let(:build) do + create(:ci_build, pipeline: pipeline, project: project, ref: 'master') + end + + let(:seed) { double('build seed', to_resource: build) } + + it 'is satisfied by changes introduced by a push' do + policy = described_class.new(['with space/*.md']) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + + it 'is not satisfied by changes that are not in the push' do + policy = described_class.new(%w[files/js/commit.js]) + + expect(policy).not_to be_satisfied_by(pipeline, seed) + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 83d39b82068..bef93fe7af7 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +require 'fast_spec_helper' +require_dependency 'active_model' describe Gitlab::Ci::Config::Entry::Policy do let(:entry) { described_class.new(config) } @@ -124,6 +125,23 @@ describe Gitlab::Ci::Config::Entry::Policy do end end + context 'when specifying a valid changes policy' do + let(:config) { { changes: %w[some/* paths/**/*.rb] } } + + it 'is a correct configuraton' do + expect(entry).to be_valid + expect(entry.value).to eq(config) + end + end + + context 'when changes policy is invalid' do + let(:config) { { changes: [1, 2] } } + + it 'returns errors' do + expect(entry.errors).to include /changes should be an array of strings/ + end + end + context 'when specifying unknown policy' do let(:config) { { refs: ['master'], invalid: :something } } diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index d75c473eb66..85b23edce9f 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1369,7 +1369,7 @@ module Gitlab end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") end - it 'returns errors if pipeline variables expression is invalid' do + it 'returns errors if pipeline variables expression policy is invalid' do config = YAML.dump({ rspec: { script: 'test', only: { variables: ['== null'] } } }) expect { Gitlab::Ci::YamlProcessor.new(config) } @@ -1377,6 +1377,14 @@ module Gitlab 'jobs:rspec:only variables invalid expression syntax') end + it 'returns errors if pipeline changes policy is invalid' do + config = YAML.dump({ rspec: { script: 'test', only: { changes: [1] } } }) + + expect { Gitlab::Ci::YamlProcessor.new(config) } + .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:only changes should be an array of strings') + end + it 'returns errors if extended hash configuration is invalid' do config = YAML.dump({ rspec: { extends: 'something', script: 'test' } }) diff --git a/spec/lib/gitlab/git/diff_stats_collection_spec.rb b/spec/lib/gitlab/git/diff_stats_collection_spec.rb index 89927cbb3a6..b07690ef39c 100644 --- a/spec/lib/gitlab/git/diff_stats_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_stats_collection_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Git::DiffStatsCollection do let(:diff_stats) { [stats_a, stats_b] } let(:collection) { described_class.new(diff_stats) } - describe '.find_by_path' do + describe '#find_by_path' do it 'returns stats by path when found' do expect(collection.find_by_path('foo')).to eq(stats_a) end @@ -23,4 +23,10 @@ describe Gitlab::Git::DiffStatsCollection do expect(collection.find_by_path('no-file')).to be_nil end end + + describe '#paths' do + it 'returns only modified paths' do + expect(collection.paths).to eq %w[foo bar] + end + end end diff --git a/spec/lib/gitlab/git/push_spec.rb b/spec/lib/gitlab/git/push_spec.rb new file mode 100644 index 00000000000..566c8209504 --- /dev/null +++ b/spec/lib/gitlab/git/push_spec.rb @@ -0,0 +1,166 @@ +require 'spec_helper' + +describe Gitlab::Git::Push do + set(:project) { create(:project, :repository) } + + let(:oldrev) { project.commit('HEAD~2').id } + let(:newrev) { project.commit.id } + let(:ref) { 'refs/heads/some-branch' } + + subject { described_class.new(project, oldrev, newrev, ref) } + + describe '#branch_name' do + context 'when it is a branch push' do + let(:ref) { 'refs/heads/my-branch' } + + it 'returns branch name' do + expect(subject.branch_name).to eq 'my-branch' + end + end + + context 'when it is a tag push' do + let(:ref) { 'refs/tags/my-branch' } + + it 'returns nil' do + expect(subject.branch_name).to be_nil + end + end + end + + describe '#branch_push?' do + context 'when pushing a branch ref' do + let(:ref) { 'refs/heads/my-branch' } + + it { is_expected.to be_branch_push } + end + + context 'when it is a tag push' do + let(:ref) { 'refs/tags/my-tag' } + + it { is_expected.not_to be_branch_push } + end + end + + describe '#branch_updated?' do + context 'when it is a branch push with correct old and new revisions' do + it { is_expected.to be_branch_updated } + end + + context 'when it is not a branch push' do + let(:ref) { 'refs/tags/my-tag' } + + it { is_expected.not_to be_branch_updated } + end + + context 'when old revision is blank' do + let(:oldrev) { Gitlab::Git::BLANK_SHA } + + it { is_expected.not_to be_branch_updated } + end + + context 'when it is not a branch push' do + let(:newrev) { Gitlab::Git::BLANK_SHA } + + it { is_expected.not_to be_branch_updated } + end + + context 'when oldrev is nil' do + let(:oldrev) { nil } + + it { is_expected.not_to be_branch_updated } + end + end + + describe '#force_push?' do + context 'when old revision is an ancestor of the new revision' do + let(:oldrev) { 'HEAD~3' } + let(:newrev) { 'HEAD~1' } + + it { is_expected.not_to be_force_push } + end + + context 'when old revision is not an ancestor of the new revision' do + let(:oldrev) { 'HEAD~3' } + let(:newrev) { '123456' } + + it { is_expected.to be_force_push } + end + end + + describe '#branch_added?' do + context 'when old revision is defined' do + it { is_expected.not_to be_branch_added } + end + + context 'when old revision is not defined' do + let(:oldrev) { Gitlab::Git::BLANK_SHA } + + it { is_expected.to be_branch_added } + end + end + + describe '#branch_removed?' do + context 'when new revision is defined' do + it { is_expected.not_to be_branch_removed } + end + + context 'when new revision is not defined' do + let(:newrev) { Gitlab::Git::BLANK_SHA } + + it { is_expected.to be_branch_removed } + end + end + + describe '#modified_paths' do + context 'when a push is a branch update' do + let(:newrev) { '498214d' } + let(:oldrev) { '281d3a7' } + + it 'returns modified paths' do + expect(subject.modified_paths).to eq ['bar/branch-test.txt', + 'files/js/commit.coffee', + 'with space/README.md'] + end + end + + context 'when a push is not a branch update' do + let(:oldrev) { Gitlab::Git::BLANK_SHA } + + it 'raises an error' do + expect { subject.modified_paths }.to raise_error(ArgumentError) + end + end + end + + describe '#oldrev' do + context 'when a valid oldrev is provided' do + it 'returns oldrev' do + expect(subject.oldrev).to eq oldrev + end + end + + context 'when a nil valud is provided' do + let(:oldrev) { nil } + + it 'returns blank SHA' do + expect(subject.oldrev).to eq Gitlab::Git::BLANK_SHA + end + end + end + + describe '#newrev' do + context 'when valid newrev is provided' do + it 'returns newrev' do + expect(subject.newrev).to eq newrev + end + end + + context 'when a nil valud is provided' do + let(:newrev) { nil } + + it 'returns blank SHA' do + expect(subject.newrev).to eq Gitlab::Git::BLANK_SHA + end + end + end +end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 1d59cff7ba8..f7935149b23 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -493,6 +493,7 @@ ProjectFeature: - snippets_access_level - builds_access_level - repository_access_level +- pages_access_level - created_at - updated_at ProtectedBranch::MergeAccessLevel: diff --git a/spec/migrations/add_pages_access_level_to_project_feature_spec.rb b/spec/migrations/add_pages_access_level_to_project_feature_spec.rb new file mode 100644 index 00000000000..3946602c5be --- /dev/null +++ b/spec/migrations/add_pages_access_level_to_project_feature_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20180423204600_add_pages_access_level_to_project_feature.rb') + +describe AddPagesAccessLevelToProjectFeature, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:features) { table(:project_features) } + let!(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab') } + let!(:first_project) { projects.create(name: 'gitlab1', path: 'gitlab1', namespace_id: namespace.id) } + let!(:first_project_features) { features.create(project_id: first_project.id) } + let!(:second_project) { projects.create(name: 'gitlab2', path: 'gitlab2', namespace_id: namespace.id) } + let!(:second_project_features) { features.create(project_id: second_project.id) } + + it 'correctly migrate pages for old projects to be public' do + migrate! + + # For old projects pages should be public + expect(first_project_features.reload.pages_access_level).to eq ProjectFeature::PUBLIC + expect(second_project_features.reload.pages_access_level).to eq ProjectFeature::PUBLIC + end + + it 'after migration pages are enabled as default' do + migrate! + + # For new project default is enabled + third_project = projects.create(name: 'gitlab3', path: 'gitlab3', namespace_id: namespace.id) + third_project_features = features.create(project_id: third_project.id) + expect(third_project_features.reload.pages_access_level).to eq ProjectFeature::ENABLED + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 5ca140879c3..3b01b39ecab 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -837,6 +837,57 @@ describe Ci::Pipeline, :mailer do end end + describe '#branch_updated?' do + context 'when pipeline has before SHA' do + before do + pipeline.update_column(:before_sha, 'a1b2c3d4') + end + + it 'runs on a branch update push' do + expect(pipeline.before_sha).not_to be Gitlab::Git::BLANK_SHA + expect(pipeline.branch_updated?).to be true + end + end + + context 'when pipeline does not have before SHA' do + before do + pipeline.update_column(:before_sha, Gitlab::Git::BLANK_SHA) + end + + it 'does not run on a branch updating push' do + expect(pipeline.branch_updated?).to be false + end + end + end + + describe '#modified_paths' do + context 'when old and new revisions are set' do + let(:project) { create(:project, :repository) } + + before do + pipeline.update(before_sha: '1234abcd', sha: '2345bcde') + end + + it 'fetches stats for changes between commits' do + expect(project.repository) + .to receive(:diff_stats).with('1234abcd', '2345bcde') + .and_call_original + + pipeline.modified_paths + end + end + + context 'when either old or new revision is missing' do + before do + pipeline.update_column(:before_sha, Gitlab::Git::BLANK_SHA) + end + + it 'raises an error' do + expect { pipeline.modified_paths }.to raise_error(ArgumentError) + end + end + end + describe '#has_kubernetes_active?' do context 'when kubernetes is active' do shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index f2aad455d5f..52c00a74b4b 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -65,7 +65,8 @@ describe InternalId do context 'with an insufficient schema version' do before do described_class.reset_column_information - expect(ActiveRecord::Migrator).to receive(:current_version).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1) + # Project factory will also call the current_version + expect(ActiveRecord::Migrator).to receive(:current_version).twice.and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1) end let(:init) { double('block') } diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index cd7f77024da..fee7d65c217 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -17,7 +17,7 @@ describe ProjectFeature do end describe '#feature_available?' do - let(:features) { %w(issues wiki builds merge_requests snippets repository) } + let(:features) { %w(issues wiki builds merge_requests snippets repository pages) } context 'when features are disabled' do it "returns false" do @@ -73,6 +73,22 @@ describe ProjectFeature do end end end + + context 'when feature is disabled by a feature flag' do + it 'returns false' do + stub_feature_flags(issues: false) + + expect(project.feature_available?(:issues, user)).to eq(false) + end + end + + context 'when feature is enabled by a feature flag' do + it 'returns true' do + stub_feature_flags(issues: true) + + expect(project.feature_available?(:issues, user)).to eq(true) + end + end end context 'repository related features' do @@ -96,6 +112,19 @@ describe ProjectFeature do end end + context 'public features' do + it "does not allow public for other than pages" do + features = %w(issues wiki builds merge_requests snippets repository) + project_feature = project.project_feature + + features.each do |feature| + field = "#{feature}_access_level".to_sym + project_feature.update_attribute(field, ProjectFeature::PUBLIC) + expect(project_feature.valid?).to be_falsy + end + end + end + describe '#*_enabled?' do let(:features) { %w(wiki builds merge_requests) } diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 1e2e13a723c..9f6cf12f9a7 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -56,6 +56,7 @@ describe API::Issues do let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } let(:no_milestone_title) { URI.escape(Milestone::None.title) } + let(:any_milestone_title) { URI.escape(Milestone::Any.title) } before(:all) do project.add_reporter(user) @@ -811,6 +812,15 @@ describe API::Issues do expect(json_response.first['id']).to eq(confidential_issue.id) end + it 'returns an array of issues with any milestone' do + get api("#{base_url}/issues?milestone=#{any_milestone_title}", user) + + response_ids = json_response.map { |issue| issue['id'] } + + expect_paginated_array_response(size: 2) + expect(response_ids).to contain_exactly(closed_issue.id, issue.id) + end + it 'sorts by created_at descending by default' do get api("#{base_url}/issues", user) diff --git a/spec/requests/api/pages/internal_access_spec.rb b/spec/requests/api/pages/internal_access_spec.rb new file mode 100644 index 00000000000..c41eabe0a48 --- /dev/null +++ b/spec/requests/api/pages/internal_access_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe "Internal Project Pages Access" do + using RSpec::Parameterized::TableSyntax + include AccessMatchers + + set(:group) { create(:group) } + set(:project) { create(:project, :internal, pages_access_level: ProjectFeature::ENABLED, namespace: group) } + + set(:admin) { create(:admin) } + set(:owner) { create(:user) } + set(:master) { create(:user) } + set(:developer) { create(:user) } + set(:reporter) { create(:user) } + set(:guest) { create(:user) } + set(:user) { create(:user) } + + before do + allow(Gitlab.config.pages).to receive(:access_control).and_return(true) + group.add_owner(owner) + project.add_master(master) + project.add_developer(developer) + project.add_reporter(reporter) + project.add_guest(guest) + end + + describe "Project should be internal" do + describe '#internal?' do + subject { project.internal? } + it { is_expected.to be_truthy } + end + end + + describe "GET /projects/:id/pages_access" do + context 'access depends on the level' do + where(:pages_access_level, :with_user, :expected_result) do + ProjectFeature::DISABLED | "admin" | 403 + ProjectFeature::DISABLED | "owner" | 403 + ProjectFeature::DISABLED | "master" | 403 + ProjectFeature::DISABLED | "developer" | 403 + ProjectFeature::DISABLED | "reporter" | 403 + ProjectFeature::DISABLED | "guest" | 403 + ProjectFeature::DISABLED | "user" | 403 + ProjectFeature::DISABLED | nil | 404 + ProjectFeature::PUBLIC | "admin" | 200 + ProjectFeature::PUBLIC | "owner" | 200 + ProjectFeature::PUBLIC | "master" | 200 + ProjectFeature::PUBLIC | "developer" | 200 + ProjectFeature::PUBLIC | "reporter" | 200 + ProjectFeature::PUBLIC | "guest" | 200 + ProjectFeature::PUBLIC | "user" | 200 + ProjectFeature::PUBLIC | nil | 404 + ProjectFeature::ENABLED | "admin" | 200 + ProjectFeature::ENABLED | "owner" | 200 + ProjectFeature::ENABLED | "master" | 200 + ProjectFeature::ENABLED | "developer" | 200 + ProjectFeature::ENABLED | "reporter" | 200 + ProjectFeature::ENABLED | "guest" | 200 + ProjectFeature::ENABLED | "user" | 200 + ProjectFeature::ENABLED | nil | 404 + ProjectFeature::PRIVATE | "admin" | 200 + ProjectFeature::PRIVATE | "owner" | 200 + ProjectFeature::PRIVATE | "master" | 200 + ProjectFeature::PRIVATE | "developer" | 200 + ProjectFeature::PRIVATE | "reporter" | 200 + ProjectFeature::PRIVATE | "guest" | 200 + ProjectFeature::PRIVATE | "user" | 403 + ProjectFeature::PRIVATE | nil | 404 + end + + with_them do + before do + project.project_feature.update(pages_access_level: pages_access_level) + end + it "correct return value" do + if !with_user.nil? + user = public_send(with_user) + get api("/projects/#{project.id}/pages_access", user) + else + get api("/projects/#{project.id}/pages_access") + end + + expect(response).to have_gitlab_http_status(expected_result) + end + end + end + end +end diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb new file mode 100644 index 00000000000..d69c15b0477 --- /dev/null +++ b/spec/requests/api/pages/private_access_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe "Private Project Pages Access" do + using RSpec::Parameterized::TableSyntax + include AccessMatchers + + set(:group) { create(:group) } + set(:project) { create(:project, :private, pages_access_level: ProjectFeature::ENABLED, namespace: group) } + + set(:admin) { create(:admin) } + set(:owner) { create(:user) } + set(:master) { create(:user) } + set(:developer) { create(:user) } + set(:reporter) { create(:user) } + set(:guest) { create(:user) } + set(:user) { create(:user) } + + before do + allow(Gitlab.config.pages).to receive(:access_control).and_return(true) + group.add_owner(owner) + project.add_master(master) + project.add_developer(developer) + project.add_reporter(reporter) + project.add_guest(guest) + end + + describe "Project should be private" do + describe '#private?' do + subject { project.private? } + it { is_expected.to be_truthy } + end + end + + describe "GET /projects/:id/pages_access" do + context 'access depends on the level' do + where(:pages_access_level, :with_user, :expected_result) do + ProjectFeature::DISABLED | "admin" | 403 + ProjectFeature::DISABLED | "owner" | 403 + ProjectFeature::DISABLED | "master" | 403 + ProjectFeature::DISABLED | "developer" | 403 + ProjectFeature::DISABLED | "reporter" | 403 + ProjectFeature::DISABLED | "guest" | 403 + ProjectFeature::DISABLED | "user" | 404 + ProjectFeature::DISABLED | nil | 404 + ProjectFeature::PUBLIC | "admin" | 200 + ProjectFeature::PUBLIC | "owner" | 200 + ProjectFeature::PUBLIC | "master" | 200 + ProjectFeature::PUBLIC | "developer" | 200 + ProjectFeature::PUBLIC | "reporter" | 200 + ProjectFeature::PUBLIC | "guest" | 200 + ProjectFeature::PUBLIC | "user" | 404 + ProjectFeature::PUBLIC | nil | 404 + ProjectFeature::ENABLED | "admin" | 200 + ProjectFeature::ENABLED | "owner" | 200 + ProjectFeature::ENABLED | "master" | 200 + ProjectFeature::ENABLED | "developer" | 200 + ProjectFeature::ENABLED | "reporter" | 200 + ProjectFeature::ENABLED | "guest" | 200 + ProjectFeature::ENABLED | "user" | 404 + ProjectFeature::ENABLED | nil | 404 + ProjectFeature::PRIVATE | "admin" | 200 + ProjectFeature::PRIVATE | "owner" | 200 + ProjectFeature::PRIVATE | "master" | 200 + ProjectFeature::PRIVATE | "developer" | 200 + ProjectFeature::PRIVATE | "reporter" | 200 + ProjectFeature::PRIVATE | "guest" | 200 + ProjectFeature::PRIVATE | "user" | 404 + ProjectFeature::PRIVATE | nil | 404 + end + + with_them do + before do + project.project_feature.update(pages_access_level: pages_access_level) + end + it "correct return value" do + if !with_user.nil? + user = public_send(with_user) + get api("/projects/#{project.id}/pages_access", user) + else + get api("/projects/#{project.id}/pages_access") + end + + expect(response).to have_gitlab_http_status(expected_result) + end + end + end + end +end diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb new file mode 100644 index 00000000000..882ca26ac51 --- /dev/null +++ b/spec/requests/api/pages/public_access_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe "Public Project Pages Access" do + using RSpec::Parameterized::TableSyntax + include AccessMatchers + + set(:group) { create(:group) } + set(:project) { create(:project, :public, pages_access_level: ProjectFeature::ENABLED, namespace: group) } + + set(:admin) { create(:admin) } + set(:owner) { create(:user) } + set(:master) { create(:user) } + set(:developer) { create(:user) } + set(:reporter) { create(:user) } + set(:guest) { create(:user) } + set(:user) { create(:user) } + + before do + allow(Gitlab.config.pages).to receive(:access_control).and_return(true) + group.add_owner(owner) + project.add_master(master) + project.add_developer(developer) + project.add_reporter(reporter) + project.add_guest(guest) + end + + describe "Project should be public" do + describe '#public?' do + subject { project.public? } + it { is_expected.to be_truthy } + end + end + + describe "GET /projects/:id/pages_access" do + context 'access depends on the level' do + where(:pages_access_level, :with_user, :expected_result) do + ProjectFeature::DISABLED | "admin" | 403 + ProjectFeature::DISABLED | "owner" | 403 + ProjectFeature::DISABLED | "master" | 403 + ProjectFeature::DISABLED | "developer" | 403 + ProjectFeature::DISABLED | "reporter" | 403 + ProjectFeature::DISABLED | "guest" | 403 + ProjectFeature::DISABLED | "user" | 403 + ProjectFeature::DISABLED | nil | 403 + ProjectFeature::PUBLIC | "admin" | 200 + ProjectFeature::PUBLIC | "owner" | 200 + ProjectFeature::PUBLIC | "master" | 200 + ProjectFeature::PUBLIC | "developer" | 200 + ProjectFeature::PUBLIC | "reporter" | 200 + ProjectFeature::PUBLIC | "guest" | 200 + ProjectFeature::PUBLIC | "user" | 200 + ProjectFeature::PUBLIC | nil | 200 + ProjectFeature::ENABLED | "admin" | 200 + ProjectFeature::ENABLED | "owner" | 200 + ProjectFeature::ENABLED | "master" | 200 + ProjectFeature::ENABLED | "developer" | 200 + ProjectFeature::ENABLED | "reporter" | 200 + ProjectFeature::ENABLED | "guest" | 200 + ProjectFeature::ENABLED | "user" | 200 + ProjectFeature::ENABLED | nil | 200 + ProjectFeature::PRIVATE | "admin" | 200 + ProjectFeature::PRIVATE | "owner" | 200 + ProjectFeature::PRIVATE | "master" | 200 + ProjectFeature::PRIVATE | "developer" | 200 + ProjectFeature::PRIVATE | "reporter" | 200 + ProjectFeature::PRIVATE | "guest" | 200 + ProjectFeature::PRIVATE | "user" | 403 + ProjectFeature::PRIVATE | nil | 403 + end + + with_them do + before do + project.project_feature.update(pages_access_level: pages_access_level) + end + it "correct return value" do + if !with_user.nil? + user = public_send(with_user) + get api("/projects/#{project.id}/pages_access", user) + else + get api("/projects/#{project.id}/pages_access") + end + + expect(response).to have_gitlab_http_status(expected_result) + end + end + end + end +end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index b7d62df0663..09c1d016081 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -785,35 +785,25 @@ describe API::Users do end describe 'GET /user/:id/keys' do - before do - admin - end + it 'returns 404 for non-existing user' do + user_id = not_existing_user_id - context 'when unauthenticated' do - it 'returns authentication error' do - get api("/users/#{user.id}/keys") - expect(response).to have_gitlab_http_status(401) - end - end + get api("/users/#{user_id}/keys") - context 'when authenticated' do - it 'returns 404 for non-existing user' do - get api('/users/999999/keys', admin) - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end - it 'returns array of ssh keys' do - user.keys << key - user.save + it 'returns array of ssh keys' do + user.keys << key + user.save - get api("/users/#{user.id}/keys", admin) + get api("/users/#{user.id}/keys") - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(key.title) - end + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(key.title) end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 56df8dddbc1..bdfb12dc5df 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -133,8 +133,9 @@ describe 'project routing' do # labels_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/labels(.:format) projects/autocomplete_sources#labels # milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones # commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands + # snippets_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/snippets(.:format) projects/autocomplete_sources#snippets describe Projects::AutocompleteSourcesController, 'routing' do - [:members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action| + [:members, :issues, :merge_requests, :labels, :milestones, :commands, :snippets].each do |action| it "to ##{action}" do expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq') end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 695b9980548..d58ff2cedc0 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -340,6 +340,27 @@ describe Projects::UpdateService do call_service end end + + context 'when updating #pages_access_level' do + subject(:call_service) do + update_project(project, admin, project_feature_attributes: { pages_access_level: ProjectFeature::PRIVATE }) + end + + it 'updates the attribute' do + expect { call_service } + .to change { project.project_feature.pages_access_level } + .to(ProjectFeature::PRIVATE) + end + + it 'calls Projects::UpdatePagesConfigurationService' do + expect(Projects::UpdatePagesConfigurationService) + .to receive(:new) + .with(project) + .and_call_original + + call_service + end + end end describe '#run_auto_devops_pipeline?' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d1337325973..cd69160be10 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -34,6 +34,11 @@ Rainbow.enabled = false # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. # Requires helpers, and shared contexts/examples first since they're used in other support files + +# Load these first since they may be required by other helpers +require Rails.root.join("spec/support/helpers/git_helpers.rb") + +# Then the rest Dir[Rails.root.join("spec/support/helpers/*.rb")].each { |f| require f } Dir[Rails.root.join("spec/support/shared_contexts/*.rb")].each { |f| require f } Dir[Rails.root.join("spec/support/shared_examples/*.rb")].each { |f| require f } diff --git a/yarn.lock b/yarn.lock index 89f6391e48b..7967ad0eb34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -616,14 +616,10 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.23.0": - version "1.29.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.29.0.tgz#03b65b513f9099bbda6ecf94d673a2952f8c6c70" - integrity sha512-sCl6nP3ph36+8P3nrw9VanAR648rgOUEBlEoLPHkhKm79xB1dUkXGBtI0uaSJVgbJx40M1/Ts8HSdMv+PF3EIg== - -"@gitlab-org/gitlab-svgs@^1.29.0": +"@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.29.0": version "1.31.0" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.31.0.tgz#495b074669f93af40e34f9978ce887773dea470a" + integrity sha512-tJbf99XX/ddFkXCXxQr9a0GJD9rPVoW3qMbU14dkxwG4WBmPEoVg+e7sLvm9OWTD1uUqiVW3qWKp++SGhhcRlw== "@gitlab-org/gitlab-ui@^1.8.0": version "1.8.0" |