diff options
Diffstat (limited to 'app')
31 files changed, 326 insertions, 142 deletions
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 |