Commit 7542a5d1 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master-ce' into scheduled-manual-jobs

parents 8deb9c01 88fa9a3c
...@@ -15,6 +15,7 @@ export const defaultAutocompleteConfig = { ...@@ -15,6 +15,7 @@ export const defaultAutocompleteConfig = {
epics: true, epics: true,
milestones: true, milestones: true,
labels: true, labels: true,
snippets: true,
}; };
class GfmAutoComplete { class GfmAutoComplete {
...@@ -50,6 +51,7 @@ class GfmAutoComplete { ...@@ -50,6 +51,7 @@ class GfmAutoComplete {
if (this.enableMap.milestones) this.setupMilestones($input); if (this.enableMap.milestones) this.setupMilestones($input);
if (this.enableMap.mergeRequests) this.setupMergeRequests($input); if (this.enableMap.mergeRequests) this.setupMergeRequests($input);
if (this.enableMap.labels) this.setupLabels($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 // We don't instantiate the quick actions autocomplete for note and issue/MR edit forms
$input.filter('[data-supports-quick-actions="true"]').atwho({ $input.filter('[data-supports-quick-actions="true"]').atwho({
...@@ -360,6 +362,39 @@ class GfmAutoComplete { ...@@ -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() { getDefaultCallbacks() {
const fetchData = this.fetchData.bind(this); const fetchData = this.fetchData.bind(this);
...@@ -470,7 +505,7 @@ class GfmAutoComplete { ...@@ -470,7 +505,7 @@ class GfmAutoComplete {
// The below is taken from At.js source // 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 // Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js // 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 atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop(); const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
...@@ -497,6 +532,7 @@ GfmAutoComplete.atTypeMap = { ...@@ -497,6 +532,7 @@ GfmAutoComplete.atTypeMap = {
'~': 'labels', '~': 'labels',
'%': 'milestones', '%': 'milestones',
'/': 'commands', '/': 'commands',
$: 'snippets',
}; };
// Emoji // Emoji
...@@ -519,7 +555,7 @@ GfmAutoComplete.Labels = { ...@@ -519,7 +555,7 @@ GfmAutoComplete.Labels = {
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>', template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
}; };
// Issues and MergeRequests // Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = { GfmAutoComplete.Issues = {
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
template: '<li><small>${id}</small> ${title}</li>', template: '<li><small>${id}</small> ${title}</li>',
......
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
<button <button
:class="{ disabled: formState.updateLoading || !isSubmitEnabled }" :class="{ disabled: formState.updateLoading || !isSubmitEnabled }"
: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" type="submit"
@click.prevent="updateIssuable"> @click.prevent="updateIssuable">
Save changes Save changes
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
v-if="shouldShowDeleteButton" v-if="shouldShowDeleteButton"
:class="{ disabled: deleteLoading }" :class="{ disabled: deleteLoading }"
: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" type="button"
@click="deleteIssuable"> @click="deleteIssuable">
Delete Delete
......
...@@ -61,7 +61,8 @@ ...@@ -61,7 +61,8 @@
ref="textarea" ref="textarea"
slot="textarea" slot="textarea"
v-model="formState.description" 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" data-supports-quick-actions="false"
aria-label="Description" aria-label="Description"
placeholder="Write a comment or drag your files here…" placeholder="Write a comment or drag your files here…"
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<input <input
id="issuable-title" id="issuable-title"
v-model="formState.title" v-model="formState.title"
class="form-control" class="form-control qa-title-input"
type="text" type="text"
placeholder="Title" placeholder="Title"
aria-label="Title" aria-label="Title"
......
...@@ -79,7 +79,8 @@ export default { ...@@ -79,7 +79,8 @@ export default {
v-if="showInlineEditButton && canUpdate" v-if="showInlineEditButton && canUpdate"
v-tooltip v-tooltip
type="button" 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" title="Edit title and description"
data-placement="bottom" data-placement="bottom"
data-container="body" data-container="body"
......
...@@ -7,7 +7,11 @@ import { __, sprintf } from '~/locale'; ...@@ -7,7 +7,11 @@ import { __, sprintf } from '~/locale';
import Flash from '../../flash'; import Flash from '../../flash';
import Autosave from '../../autosave'; import Autosave from '../../autosave';
import TaskList from '../../task_list'; 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 * as constants from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
...@@ -122,7 +126,9 @@ export default { ...@@ -122,7 +126,9 @@ export default {
return this.getNoteableData.create_note_path; return this.getNoteableData.create_note_path;
}, },
issuableTypeTitle() { issuableTypeTitle() {
return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE ? 'merge request' : 'issue'; return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
? 'merge request'
: 'issue';
}, },
}, },
watch: { watch: {
...@@ -359,7 +365,7 @@ Please check your network connection and try again.`; ...@@ -359,7 +365,7 @@ Please check your network connection and try again.`;
:disabled="isSubmitting" :disabled="isSubmitting"
name="note[note]" name="note[note]"
class="note-textarea js-vue-comment-form js-note-text 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" data-supports-quick-actions="true"
aria-label="Description" aria-label="Description"
placeholder="Write a comment or drag your files here…" placeholder="Write a comment or drag your files here…"
...@@ -374,7 +380,8 @@ js-gfm-input js-autosize markdown-area js-vue-textarea" ...@@ -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"> append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
<button <button
:disabled="isSubmitButtonDisabled" :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" type="submit"
@click.prevent="handleSave()"> @click.prevent="handleSave()">
{{ __(commentButtonTitle) }} {{ __(commentButtonTitle) }}
......
...@@ -52,6 +52,21 @@ ...@@ -52,6 +52,21 @@
required: false, required: false,
default: '', default: '',
}, },
pagesAvailable: {
type: Boolean,
required: false,
default: false,
},
pagesAccessControlEnabled: {
type: Boolean,
required: false,
default: false,
},
pagesHelpPath: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
...@@ -64,6 +79,7 @@ ...@@ -64,6 +79,7 @@
buildsAccessLevel: 20, buildsAccessLevel: 20,
wikiAccessLevel: 20, wikiAccessLevel: 20,
snippetsAccessLevel: 20, snippetsAccessLevel: 20,
pagesAccessLevel: 20,
containerRegistryEnabled: true, containerRegistryEnabled: true,
lfsEnabled: true, lfsEnabled: true,
requestAccessEnabled: true, requestAccessEnabled: true,
...@@ -90,6 +106,13 @@ ...@@ -90,6 +106,13 @@
); );
}, },
pagesFeatureAccessLevelOptions() {
if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
return this.featureAccessLevelOptions.concat([[30, 'Everyone']]);
}
return this.featureAccessLevelOptions;
},
repositoryEnabled() { repositoryEnabled() {
return this.repositoryAccessLevel > 0; return this.repositoryAccessLevel > 0;
}, },
...@@ -109,6 +132,10 @@ ...@@ -109,6 +132,10 @@
this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); 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(); this.highlightChanges();
} else if (oldValue === visibilityOptions.PRIVATE) { } else if (oldValue === visibilityOptions.PRIVATE) {
// if changing away from private, make enabled features more permissive // if changing away from private, make enabled features more permissive
...@@ -118,6 +145,7 @@ ...@@ -118,6 +145,7 @@
if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
this.highlightChanges(); this.highlightChanges();
} }
}, },
...@@ -323,6 +351,18 @@ ...@@ -323,6 +351,18 @@
name="project[project_feature_attributes][snippets_access_level]" name="project[project_feature_attributes][snippets_access_level]"
/> />
</project-setting-row> </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>
</div> </div>
</template> </template>
...@@ -11,6 +11,7 @@ export default () => { ...@@ -11,6 +11,7 @@ export default () => {
epics: false, epics: false,
milestones: false, milestones: false,
labels: false, labels: false,
snippets: false,
}); });
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
}; };
...@@ -15,5 +15,6 @@ export default (initGFM = true) => { ...@@ -15,5 +15,6 @@ export default (initGFM = true) => {
epics: initGFM, epics: initGFM,
milestones: initGFM, milestones: initGFM,
labels: initGFM, labels: initGFM,
snippets: initGFM,
}); });
}; };
...@@ -76,6 +76,7 @@ ...@@ -76,6 +76,7 @@
epics: this.enableAutocomplete, epics: this.enableAutocomplete,
milestones: this.enableAutocomplete, milestones: this.enableAutocomplete,
labels: this.enableAutocomplete, labels: this.enableAutocomplete,
snippets: this.enableAutocomplete,
}); });
}, },
beforeDestroy() { beforeDestroy() {
......
...@@ -27,6 +27,10 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController ...@@ -27,6 +27,10 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
render json: @autocomplete_service.commands(target, params[:type]) render json: @autocomplete_service.commands(target, params[:type])
end end
def snippets
render json: @autocomplete_service.snippets
end
private private
def load_autocomplete_service def load_autocomplete_service
......
...@@ -366,6 +366,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -366,6 +366,7 @@ class ProjectsController < Projects::ApplicationController
repository_access_level repository_access_level
snippets_access_level snippets_access_level
wiki_access_level wiki_access_level
pages_access_level
] ]
] ]
end end
......
...@@ -363,6 +363,7 @@ class IssuableFinder ...@@ -363,6 +363,7 @@ class IssuableFinder
def use_cte_for_search? def use_cte_for_search?
return false unless search return false unless search
return false unless Gitlab::Database.postgresql? 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] params[:use_cte_for_search]
end end
...@@ -428,6 +429,10 @@ class IssuableFinder ...@@ -428,6 +429,10 @@ class IssuableFinder
params[:milestone_title] == Milestone::Upcoming.name params[:milestone_title] == Milestone::Upcoming.name
end end
def filter_by_any_milestone?
params[:milestone_title] == Milestone::Any.title
end
def filter_by_started_milestone? def filter_by_started_milestone?
params[:milestone_title] == Milestone::Started.name params[:milestone_title] == Milestone::Started.name
end end
...@@ -437,6 +442,8 @@ class IssuableFinder ...@@ -437,6 +442,8 @@ class IssuableFinder
if milestones? if milestones?
if filter_by_no_milestone? if filter_by_no_milestone?
items = items.left_joins_milestones.where(milestone_id: [-1, nil]) items = items.left_joins_milestones.where(milestone_id: [-1, nil])
elsif filter_by_any_milestone?
items = items.any_milestone
elsif filter_by_upcoming_milestone? elsif filter_by_upcoming_milestone?
upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items)) upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
items = items.left_joins_milestones.where(milestone_id: upcoming_ids) items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
......
...@@ -120,9 +120,13 @@ class IssuesFinder < IssuableFinder ...@@ -120,9 +120,13 @@ class IssuesFinder < IssuableFinder
return @user_can_see_all_confidential_issues = true if current_user.full_private_access? return @user_can_see_all_confidential_issues = true if current_user.full_private_access?
@user_can_see_all_confidential_issues = @user_can_see_all_confidential_issues =
project? && if project? && project
project && project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL
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 end
def user_cannot_see_confidential_issues? def user_cannot_see_confidential_issues?
......
...@@ -129,7 +129,7 @@ class LabelsFinder < UnionFinder ...@@ -129,7 +129,7 @@ class LabelsFinder < UnionFinder
end end
def project? def project?
params[:project_id].present? params[:project].present? || params[:project_id].present?
end end
def projects? def projects?
...@@ -152,7 +152,7 @@ class LabelsFinder < UnionFinder ...@@ -152,7 +152,7 @@ class LabelsFinder < UnionFinder
return @project if defined?(@project) return @project if defined?(@project)
if 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) @project = nil unless authorized_to_read_labels?(@project)
else else
@project = nil @project = nil
......
...@@ -16,7 +16,7 @@ module Types ...@@ -16,7 +16,7 @@ module Types
:create_deployment, :push_to_delete_protected_branch, :create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages, :admin_wiki, :admin_project, :update_pages,
:admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki, :admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
:create_pages, :destroy_pages :create_pages, :destroy_pages, :read_pages_content
end end
end end
end end
...@@ -292,7 +292,8 @@ module ApplicationHelper ...@@ -292,7 +292,8 @@ module ApplicationHelper
mergeRequests: merge_requests_project_autocomplete_sources_path(object), mergeRequests: merge_requests_project_autocomplete_sources_path(object),
labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]), labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
milestones: milestones_project_autocomplete_sources_path(object), 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
end end
...@@ -454,6 +454,7 @@ module ProjectsHelper ...@@ -454,6 +454,7 @@ module ProjectsHelper
buildsAccessLevel: feature.builds_access_level, buildsAccessLevel: feature.builds_access_level,
wikiAccessLevel: feature.wiki_access_level, wikiAccessLevel: feature.wiki_access_level,
snippetsAccessLevel: feature.snippets_access_level, snippetsAccessLevel: feature.snippets_access_level,
pagesAccessLevel: feature.pages_access_level,
containerRegistryEnabled: !!project.container_registry_enabled, containerRegistryEnabled: !!project.container_registry_enabled,
lfsEnabled: !!project.lfs_enabled lfsEnabled: !!project.lfs_enabled
} }
...@@ -468,7 +469,10 @@ module ProjectsHelper ...@@ -468,7 +469,10 @@ module ProjectsHelper
registryAvailable: Gitlab.config.registry.enabled, registryAvailable: Gitlab.config.registry.enabled,
registryHelpPath: help_page_path('user/project/container_registry'), registryHelpPath: help_page_path('user/project/container_registry'),
lfsAvailable: Gitlab.config.lfs.enabled, 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 end
......
...@@ -633,6 +633,18 @@ module Ci ...@@ -633,6 +633,18 @@ module Ci
end end
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? def default_branch?
ref == project.default_branch ref == project.default_branch
end end
...@@ -660,6 +672,22 @@ module Ci ...@@ -660,6 +672,22 @@ module Ci
Gitlab::DataBuilder::Pipeline.build(self) Gitlab::DataBuilder::Pipeline.build(self)
end 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 def latest_builds_status
return 'failed' unless yaml_errors.blank? return 'failed' unless yaml_errors.blank?
......
# 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
...@@ -76,6 +76,7 @@ module Issuable ...@@ -76,6 +76,7 @@ module Issuable
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_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 :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :opened, -> { with_state(:opened) } scope :opened, -> { with_state(:opened) }
scope :only_opened, -> { with_state(:opened) } scope :only_opened, -> { with_state(:opened) }
......
...@@ -5,14 +5,11 @@ ...@@ -5,14 +5,11 @@
# A note of this type can be resolvable. # A note of this type can be resolvable.
class DiffNote < Note class DiffNote < Note
include NoteOnDiff include NoteOnDiff
include DiffPositionableNote
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
NOTEABLE_TYPES = %w(MergeRequest Commit).freeze 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 :original_position, presence: true
validates :position, presence: true validates :position, presence: true
validates :line_code, presence: true, line_code: true, if: :on_text? validates :line_code, presence: true, line_code: true, if: :on_text?
...@@ -21,8 +18,6 @@ class DiffNote < Note ...@@ -21,8 +18,6 @@ class DiffNote < Note
validate :verify_supported validate :verify_supported
validate :diff_refs_match_commit, if: :for_commit? 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? before_validation :set_line_code, if: :on_text?
after_save :keep_around_commits after_save :keep_around_commits
after_commit :create_diff_file, on: :create after_commit :create_diff_file, on: :create
...@@ -31,31 +26,6 @@ class DiffNote < Note ...@@ -31,31 +26,6 @@ class DiffNote < Note
DiffDiscussion DiffDiscussion
end 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 def create_diff_file
return unless should_create_diff_file? return unless should_create_diff_file?
...@@ -87,15 +57,6 @@ class DiffNote < Note ...@@ -87,15 +57,6 @@ class DiffNote < Note
self.diff_file.line_code(self.diff_line) self.diff_file.line_code(self.diff_line)
end 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) def created_at_diff?(diff_refs)
return false unless supported? return false unless supported?
return true if for_commit? return true if for_commit?
...@@ -141,37 +102,10 @@ class DiffNote < Note ...@@ -141,37 +102,10 @@ class DiffNote < Note
for_commit? || self.noteable.has_complete_diff_refs? for_commit? || self.noteable.has_complete_diff_refs?
end end
def set_original_position
self.original_position = self.position.dup unless self.original_position&.complete?
end
def set_line_code def set_line_code
self.line_code = self.position.line_code(self.project.repository) self.line_code = self.position.line_code(self.project.repository)
end 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 def verify_supported
return if supported? return if supported?
......
...@@ -55,8 +55,8 @@ class Project < ActiveRecord::Base ...@@ -55,8 +55,8 @@ class Project < ActiveRecord::Base
cache_markdown_field :description, pipeline: :description cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
:merge_requests_enabled?, :issues_enabled?, to: :project_feature, :merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?,
allow_nil: true to: :project_feature, allow_nil: true
delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
...@@ -356,7 +356,7 @@ class Project < ActiveRecord::Base ...@@ -356,7 +356,7 @@ class Project < ActiveRecord::Base
# "enabled" here means "not disabled". It includes private features! # "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) { scope :with_feature_enabled, ->(feature) {
access_level_attribute = ProjectFeature.access_level_attribute(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. # Picks a feature where the level is exactly that given.
...@@ -418,15 +418,15 @@ class Project < ActiveRecord::Base ...@@ -418,15 +418,15 @@ class Project < ActiveRecord::Base
end end
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 # 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 # This method uses an optimised version of `with_feature_access_level` for
# logged in users to more efficiently get private projects with the given # logged in users to more efficiently get private projects with the given
# feature. # feature.
def self.with_feature_available_for_user(feature, user) def self.with_feature_available_for_user(feature, user)
visible = [nil, ProjectFeature::ENABLED] visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
if user&.admin? if user&.admin?
with_feature_enabled(feature) with_feature_enabled(feature)
......
...@@ -13,14 +13,16 @@ class ProjectFeature < ActiveRecord::Base ...@@ -13,14 +13,16 @@ class ProjectFeature < ActiveRecord::Base
# Disabled: not enabled for anyone # Disabled: not enabled for anyone
# Private: enabled only for team members # Private: enabled only for team members
# Enabled: enabled for everyone able to access the project # Enabled: enabled for everyone able to access the project
# Public: enabled for everyone (only allowed for pages)
# #
# Permission levels # Permission levels
DISABLED = 0 DISABLED = 0
PRIVATE = 10 PRIVATE = 10
ENABLED = 20 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 class << self
def access_level_attribute(feature) def access_level_attribute(feature)
...@@ -46,6 +48,7 @@ class ProjectFeature < ActiveRecord::Base ...@@ -46,6 +48,7 @@ class ProjectFeature < ActiveRecord::Base
validates :project, presence: true validates :project, presence: true
validate :repository_children_level validate :repository_children_level
validate :allowed_access_levels
default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_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 ...@@ -55,6 +58,9 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :repository_access_level, value: ENABLED, allows_nil: false default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user) 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)) get_permission(user, access_level(feature))
end end
...@@ -78,6 +84,16 @@ class ProjectFeature < ActiveRecord::Base ...@@ -78,6 +84,16 @@ class ProjectFeature < ActiveRecord::Base
issues_access_level > DISABLED issues_access_level > DISABLED
end 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 private
# Validates builds and merge requests access level # Validates builds and merge requests access level
...@@ -92,6 +108,17 @@ class ProjectFeature < ActiveRecord::Base ...@@ -92,6 +108,17 @@ class ProjectFeature < ActiveRecord::Base
%i(merge_requests_access_level builds_access_level).each(&validator) %i(merge_requests_access_level builds_access_level).each(&validator)
end 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) def get_permission(user, level)
case level case level
when DISABLED when DISABLED
...@@ -100,6 +127,8 @@ class ProjectFeature < ActiveRecord::Base ...@@ -100,6 +127,8 @@ class ProjectFeature < ActiveRecord::Base
user && (project.team.member?(user) || user.full_private_access?) user && (project.team.member?(user) || user.full_private_access?)
when ENABLED when ENABLED
true true
when PUBLIC
true
else else
true true
end end
......
...@@ -110,6 +110,7 @@ class ProjectPolicy < BasePolicy ...@@ -110,6 +110,7 @@ class ProjectPolicy < BasePolicy
snippets snippets
wiki wiki
builds builds
pages
] ]
features.each do |f| features.each do |f|
...@@ -167,6 +168,7 @@ class ProjectPolicy < BasePolicy ...@@ -167,6 +168,7 @@ class ProjectPolicy < BasePolicy
enable :upload_file enable :upload_file
enable :read_cycle_analytics enable :read_cycle_analytics
enable :award_emoji enable :award_emoji
enable :read_pages_content
end end
# These abilities are not allowed to admins that are not members of the project, # These abilities are not allowed to admins that are not members of the project,
...@@ -286,6 +288,8 @@ class ProjectPolicy < BasePolicy ...@@ -286,6 +288,8 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:merge_request)) prevent(*create_read_update_admin_destroy(:merge_request))
end end
rule { pages_disabled }.prevent :read_pages_content
rule { issues_disabled & merge_requests_disabled }.policy do rule { issues_disabled & merge_requests_disabled }.policy do
prevent(*create_read_update_admin_destroy(:label)) prevent(*create_read_update_admin_destroy(:label))
prevent(*create_read_update_admin_destroy(:milestone)) prevent(*create_read_update_admin_destroy(:milestone))
...@@ -345,6 +349,7 @@ class ProjectPolicy < BasePolicy ...@@ -345,6 +349,7 @@ class ProjectPolicy < BasePolicy
enable :download_code enable :download_code
enable :download_wiki_code enable :download_wiki_code
enable :read_cycle_analytics enable :read_cycle_analytics
enable :read_pages_content
# NOTE: may be overridden by IssuePolicy # NOTE: may be overridden by IssuePolicy
enable :read_issue enable :read_issue
......
# frozen_string_literal: true # frozen_string_literal: true
class BuildDetailsEntity < JobEntity class BuildDetailsEntity < JobEntity
include EnvironmentHelper
include RequestAwareEntity
include CiStatusHelper
expose :coverage, :erased_at, :duration expose :coverage, :erased_at, :duration
expose :tag_list, as: :tags expose :tag_list, as: :tags
expose :has_trace?, as: :has_trace expose :has_trace?, as: :has_trace
...@@ -15,10 +11,6 @@ class BuildDetailsEntity < JobEntity ...@@ -15,10 +11,6 @@ class BuildDetailsEntity < JobEntity
expose :deployment_status, if: -> (*) { build.has_environment? } do expose :deployment_status, if: -> (*) { build.has_environment? } do
expose :deployment_status, as: :status expose :deployment_status, as: :status
expose :icon do |build|
ci_label_for_status(build.status)
end
expose :persisted_environment, as: :environment, with: EnvironmentEntity expose :persisted_environment, as: :environment, with: EnvironmentEntity
end end
......
...@@ -57,10 +57,10 @@ module MergeRequests ...@@ -57,10 +57,10 @@ module MergeRequests
# Returns all origin and fork merge requests from `@project` satisfying passed arguments. # Returns all origin and fork merge requests from `@project` satisfying passed arguments.
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def merge_requests_for(source_branch, mr_states: [:opened]) def merge_requests_for(source_branch, mr_states: [:opened])
MergeRequest @project.source_of_merge_requests
.with_state(mr_states) .with_state(mr_states)
.where(source_branch: source_branch, source_project_id: @project.id) .where(source_branch: source_branch)
.preload(:source_project) # we don't need a #includes since we're just preloading for the #select .preload(:source_project) # we don't need #includes since we're just preloading for the #select
.select(&:source_project) .select(&:source_project)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -3,17 +3,16 @@ ...@@ -3,17 +3,16 @@
module MergeRequests module MergeRequests
class RefreshService < MergeRequests::BaseService class RefreshService < MergeRequests::BaseService
def execute(oldrev, newrev, ref) 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 end
private private
def do_execute(oldrev, newrev, ref) def refresh_merge_requests!
@oldrev, @newrev = oldrev, newrev
@branch_name = Gitlab::Git.ref_name(ref)
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits)) Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an # Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge # empty diff during a manual merge
...@@ -25,7 +24,7 @@ module MergeRequests ...@@ -25,7 +24,7 @@ module MergeRequests
cache_merge_requests_closing_issues cache_merge_requests_closing_issues
# Leave a system note if a branch was deleted/added # 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 comment_mr_branch_presence_changed
end end
...@@ -54,8 +53,10 @@ module MergeRequests ...@@ -54,8 +53,10 @@ module MergeRequests
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def post_merge_manually_merged def post_merge_manually_merged
commit_ids = @commits.map(&:id) 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 = @project.merge_requests.opened
merge_requests = merge_requests.select(&:diff_head_commit) .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| merge_requests = merge_requests.select do |merge_request|
commit_ids.include?(merge_request.diff_head_sha) && commit_ids.include?(merge_request.diff_head_sha) &&
...@@ -70,24 +71,20 @@ module MergeRequests ...@@ -70,24 +71,20 @@ module MergeRequests
end end
# rubocop: enable CodeReuse/ActiveRecord # 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 # Refresh merge request diff if we push to source or target branch of merge request
# Note: we should update merge requests from forks too # Note: we should update merge requests from forks too
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def reload_merge_requests def reload_merge_requests
merge_requests = @project.merge_requests.opened 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 # Fork merge requests
merge_requests += MergeRequest.opened 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 .where.not(target_project: @project).to_a
filter_merge_requests(merge_requests).each do |merge_request| 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) merge_request.reload_diff(current_user)
else else
mr_commit_ids = merge_request.commit_shas mr_commit_ids = merge_request.commit_shas
...@@ -117,7 +114,7 @@ module MergeRequests ...@@ -117,7 +114,7 @@ module MergeRequests
end end
def find_new_commits def find_new_commits
if branch_added? if @push.branch_added?
@commits = [] @commits = []
merge_request = merge_requests_for_source_branch.first merge_request = merge_requests_for_source_branch.first
...@@ -126,28 +123,28 @@ module MergeRequests ...@@ -126,28 +123,28 @@ module MergeRequests
begin begin
# Since any number of commits could have been made to the restored branch, # Since any number of commits could have been made to the restored branch,
# find the common root to see what has been added. # 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 # 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 # 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 rescue
end end
elsif branch_removed? elsif @push.branch_removed?
# No commits for a deleted branch. # No commits for a deleted branch.
@commits = [] @commits = []
else else
@commits = @project.repository.commits_between(@oldrev, @newrev) @commits = @project.repository.commits_between(@push.oldrev, @push.newrev)
end end
end end
# Add comment about branches being deleted or added to merge requests # Add comment about branches being deleted or added to merge requests
def comment_mr_branch_presence_changed 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| merge_requests_for_source_branch.each do |merge_request|
SystemNoteService.change_branch_presence( SystemNoteService.change_branch_presence(
merge_request, merge_request.project, @current_user, merge_request, merge_request.project, @current_user,
:source, @branch_name, presence) :source, @push.branch_name, presence)
end end
end end
...@@ -164,7 +161,7 @@ module MergeRequests ...@@ -164,7 +161,7 @@ module MergeRequests
SystemNoteService.add_commits(merge_request, merge_request.project, SystemNoteService.add_commits(merge_request, merge_request.project,
@current_user, new_commits, @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) notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits)
end end
...@@ -195,7 +192,7 @@ module MergeRequests ...@@ -195,7 +192,7 @@ module MergeRequests
# Call merge request webhook with update branches # Call merge request webhook with update branches
def execute_mr_web_hooks def execute_mr_web_hooks
merge_requests_for_source_branch.each do |merge_request| 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
end end
...@@ -203,7 +200,7 @@ module MergeRequests ...@@ -203,7 +200,7 @@ module MergeRequests
# `MergeRequestsClosingIssues` model (as a performance optimization). # `MergeRequestsClosingIssues` model (as a performance optimization).
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def cache_merge_requests_closing_issues 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) merge_request.cache_merge_request_closes_issues!(@current_user)
end end
end end
...@@ -215,15 +212,7 @@ module MergeRequests ...@@ -215,15 +212,7 @@ module MergeRequests
def merge_requests_for_source_branch(reload: false) def merge_requests_for_source_branch(reload: false)
@source_merge_requests = nil if reload @source_merge_requests = nil if reload
@source_merge_requests ||= merge_requests_for(@branch_name) @source_merge_requests ||= merge_requests_for(@push.branch_name)
end
def branch_added?
Gitlab::Git.blank_ref?(@oldrev)
end
def branch_removed?
Gitlab::Git.blank_ref?(@newrev)
end end
end end
end end
...@@ -29,6 +29,10 @@ module Projects ...@@ -29,6 +29,10 @@ module Projects
QuickActions::InterpretService.new(project, current_user).available_commands(noteable) QuickActions::InterpretService.new(project, current_user).available_commands(noteable)
end end
def snippets
SnippetsFinder.new(current_user, project: project).execute.select([:id, :title])
end
def labels_as_hash(target) def labels_as_hash(target)
super(target, project_id: project.id, include_ancestor_groups: true) super(target, project_id: project.id, include_ancestor_groups: true)
end end
......
...@@ -21,7 +21,9 @@ module Projects ...@@ -21,7 +21,9 @@ module Projects
def pages_config def pages_config
{ {
domains: pages_domains_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 end
...@@ -31,7 +33,9 @@ module Projects ...@@ -31,7 +33,9 @@ module Projects
domain: domain.domain, domain: domain.domain,
certificate: domain.certificate, certificate: domain.certificate,
key: domain.key, 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
end end
......
...@@ -72,7 +72,11 @@ module Projects ...@@ -72,7 +72,11 @@ module Projects
system_hook_service.execute_hooks_for(project, :update) system_hook_service.execute_hooks_for(project, :update)
end 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 end
def update_failed! def update_failed!
...@@ -102,6 +106,10 @@ module Projects ...@@ -102,6 +106,10 @@ module Projects
params.dig(:project_feature_attributes, :wiki_access_level).to_i > ProjectFeature::DISABLED params.dig(:project_feature_attributes, :wiki_access_level).to_i > ProjectFeature::DISABLED
end end
def changing_pages_access_level?
params.dig(:project_feature_attributes, :pages_access_level)
end
def ensure_wiki_exists def ensure_wiki_exists
ProjectWiki.new(project, project.owner).wiki ProjectWiki.new(project, project.owner).wiki
rescue ProjectWiki::CouldNotCreateWikiError rescue ProjectWiki::CouldNotCreateWikiError
......
---
title: Add autocomplete drop down filter for project snippets
merge_request: 21458
author: Fabian Schneider
type: added
---
title: Fix sorting by priority or popularity on group issues page, when also searching
issue content
merge_request: 21521
author:
type: fixed
---
title: Allows to filter issues by Any milestone in the API
merge_request: 22080
author: Jacopo Beschi @jacopo-beschi
type: added
---
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
---
title: Add support for pipeline only/except policy for modified paths
merge_request: 21981
author:
type: added
---
title: Enable unauthenticated access to public SSH keys via the API
merge_request: 20118
author: Ronald Claveau
type: changed
---
title: Banzai label ref finder - minimize SQL calls by sharing context more aggresively
merge_request: 22070
author:
type: performance
...@@ -210,6 +210,7 @@ production: &base ...@@ -210,6 +210,7 @@ production: &base
## GitLab Pages ## GitLab Pages
pages: pages:
enabled: false enabled: false
access_control: false
# The location where pages are stored (default: shared/pages). # The location where pages are stored (default: shared/pages).
# path: shared/pages # path: shared/pages
......
...@@ -200,6 +200,7 @@ Settings.registry['path'] = Settings.absolute(Settings.registry['path ...@@ -200,6 +200,7 @@ Settings.registry['path'] = Settings.absolute(Settings.registry['path
# #
Settings['pages'] ||= Settingslogic.new({}) Settings['pages'] ||= Settingslogic.new({})
Settings.pages['enabled'] = false if Settings.pages['enabled'].nil? 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['path'] = Settings.absolute(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"))
Settings.pages['https'] = false if Settings.pages['https'].nil? Settings.pages['https'] = false if Settings.pages['https'].nil?
Settings.pages['host'] ||= "example.com" Settings.pages['host'] ||= "example.com"
......
...@@ -32,6 +32,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -32,6 +32,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'labels' get 'labels'
get 'milestones' get 'milestones'
get 'commands' get 'commands'
get 'snippets'
end end
end end
......
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
...@@ -1580,6 +1580,7 @@ ActiveRecord::Schema.define(version: 20180924201039) do ...@@ -1580,6 +1580,7 @@ ActiveRecord::Schema.define(version: 20180924201039) do
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "repository_access_level", default: 20, null: false t.integer "repository_access_level", default: 20, null: false
t.integer "pages_access_level", default: 20, null: false
end end
add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", unique: true, using: :btree add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", unique: true, using: :btree
......
...@@ -92,9 +92,8 @@ where `example.io` is the domain under which GitLab Pages will be served ...@@ -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 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. IPv6 address. If you don't have IPv6, you can omit the AAAA record.
> **Note:** NOTE: **Note:**
You should not use the GitLab domain to serve user pages. For more information You should not use the GitLab domain to serve user pages. For more information see the [security section](#security).
see the [security section](#security).
[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record [wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record
...@@ -107,12 +106,13 @@ since that is needed in all configurations. ...@@ -107,12 +106,13 @@ since that is needed in all configurations.
### Wildcard domains ### Wildcard domains
> **Requirements:** **Requirements:**
> - [Wildcard DNS setup](#dns-configuration)
> - [Wildcard DNS setup](#dns-configuration)
> ---
> ---
> URL scheme: `http://page.example.io`
URL scheme: `http://page.example.io`
This is the minimum setup that you can use Pages with. It is the base for all 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. 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. ...@@ -126,18 +126,18 @@ The Pages daemon doesn't listen to the outside world.
1. [Reconfigure GitLab][reconfigure] 1. [Reconfigure GitLab][reconfigure]
Watch the [video tutorial][video-admin] for this configuration. Watch the [video tutorial][video-admin] for this configuration.
### Wildcard domains with TLS support ### Wildcard domains with TLS support
> **Requirements:** **Requirements:**
> - [Wildcard DNS setup](#dns-configuration)
> - Wildcard TLS certificate - [Wildcard DNS setup](#dns-configuration)
> - Wildcard TLS certificate
> ---
> ---
> URL scheme: `https://page.example.io`
URL scheme: `https://page.example.io`
Nginx will proxy all requests to the daemon. Pages daemon doesn't listen to the Nginx will proxy all requests to the daemon. Pages daemon doesn't listen to the
outside world. outside world.
...@@ -168,13 +168,14 @@ you have IPv6 as well as IPv4 addresses, you can use them both. ...@@ -168,13 +168,14 @@ you have IPv6 as well as IPv4 addresses, you can use them both.
### Custom domains ### Custom domains
> **Requirements:** **Requirements:**
> - [Wildcard DNS setup](#dns-configuration)
> - Secondary IP - [Wildcard DNS setup](#dns-configuration)
> - Secondary IP
> ---
> ---
> URL scheme: `http://page.example.io` and `http://domain.com`
URL scheme: `http://page.example.io` and `http://domain.com`
In that case, the Pages daemon is running, Nginx still proxies requests to 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 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. ...@@ -197,14 +198,15 @@ world. Custom domains are supported, but no TLS.
### Custom domains with TLS support ### Custom domains with TLS support
> **Requirements:** **Requirements:**
> - [Wildcard DNS setup](#dns-configuration)
> - Wildcard TLS certificate - [Wildcard DNS setup](#dns-configuration)
> - Secondary IP - Wildcard TLS certificate
> - Secondary IP
> ---
> ---
> URL scheme: `https://page.example.io` and `https://domain.com`
URL scheme: `https://page.example.io` and `https://domain.com`
In that case, the Pages daemon is running, Nginx still proxies requests to 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 the daemon but the daemon is also able to receive requests from the outside
...@@ -320,12 +322,12 @@ latest previous version. ...@@ -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. - GitLab Pages were ported to Community Edition in GitLab 8.17.
- Documentation was refactored to be more modular and easy to follow. - 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 - In GitLab 8.5 we introduced the [gitlab-pages][] daemon which is now the
recommended way to set up GitLab Pages. recommended way to set up GitLab Pages.
...@@ -334,13 +336,10 @@ latest previous version. ...@@ -334,13 +336,10 @@ latest previous version.
- Custom CNAME and TLS certificates support. - Custom CNAME and TLS certificates support.
- Documentation was moved to one place. - 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. - 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 [backup]: ../../raketasks/backup_restore.md
[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 [ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
......
...@@ -37,7 +37,7 @@ GET /issues?my_reaction_emoji=star ...@@ -37,7 +37,7 @@ GET /issues?my_reaction_emoji=star
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `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 | | `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)_ | | `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)_ | | `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)_ | | `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
......
...@@ -558,7 +558,7 @@ Parameters: ...@@ -558,7 +558,7 @@ Parameters:
## List SSH keys for user ## 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 GET /users/:id/keys
......
...@@ -387,6 +387,8 @@ except master. ...@@ -387,6 +387,8 @@ except master.
> `refs` and `kubernetes` policies introduced in GitLab 10.0 > `refs` and `kubernetes` policies introduced in GitLab 10.0
> >
> `variables` policy introduced in 10.7 > `variables` policy introduced in 10.7
>
> `changes` policy [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/19232) in 11.4
CAUTION: **Warning:** CAUTION: **Warning:**
This an _alpha_ feature, and it it subject to change at any time without This an _alpha_ feature, and it it subject to change at any time without
...@@ -398,10 +400,15 @@ policy configuration. ...@@ -398,10 +400,15 @@ policy configuration.
GitLab now supports both, simple and complex strategies, so it is possible to GitLab now supports both, simple and complex strategies, so it is possible to
use an array and a hash configuration scheme. 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 Refs strategy equals to simplified only/except configuration, whereas
kubernetes strategy accepts only `active` keyword. kubernetes strategy accepts only `active` keyword.
### `variables`
`variables` keyword is used to define variables expressions. In other words `variables` keyword is used to define variables expressions. In other words
you can use predefined variables / project / group or you can use predefined variables / project / group or
environment-scoped variables to define an expression GitLab is going to environment-scoped variables to define an expression GitLab is going to
...@@ -445,6 +452,46 @@ end-to-end: ...@@ -445,6 +452,46 @@ end-to-end:
Learn more about variables expressions on [a separate page][variables-expressions]. 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`
`tags` is used to select specific Runners from the list of all Runners that are `tags` is used to select specific Runners from the list of all Runners that are
......
...@@ -60,7 +60,7 @@ people. ...@@ -60,7 +60,7 @@ people.
The current team labels are: The current team labels are:
- ~Configuration - ~Configure
- ~"CI/CD" - ~"CI/CD"
- ~Create - ~Create
- ~Distribution - ~Distribution
......
...@@ -110,6 +110,7 @@ which visibility level you select on project settings. ...@@ -110,6 +110,7 @@ which visibility level you select on project settings.
- Disabled: disabled for everyone - Disabled: disabled for everyone
- Only team members: only team members will see even if your project is public or internal - 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 with access: everyone can see depending on your project visibility level
- Everyone: enabled for everyone (only available for GitLab Pages)
### Protected branches ### Protected branches
...@@ -242,6 +243,7 @@ which visibility level you select on project settings. ...@@ -242,6 +243,7 @@ which visibility level you select on project settings.
- Disabled: disabled for everyone - Disabled: disabled for everyone
- Only team members: only team members will see even if your project is public or internal - 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 with access: everyone can see depending on your project visibility level
- Everyone: enabled for everyone (only available for GitLab Pages)
## GitLab CI/CD permissions ## GitLab CI/CD permissions
......
...@@ -287,6 +287,12 @@ module API ...@@ -287,6 +287,12 @@ module API
present_projects forks present_projects forks
end 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 desc 'Update an existing project' do
success Entities::Project success Entities::Project
end end
......
...@@ -256,7 +256,7 @@ module API ...@@ -256,7 +256,7 @@ module API
end end
# rubocop: enable CodeReuse/ActiveRecord # 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 success Entities::SSHKey
end end
params do params do
...@@ -265,10 +265,8 @@ module API ...@@ -265,10 +265,8 @@ module API
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
get ':id/keys' do get ':id/keys' do
authenticated_as_admin!
user = User.find_by(id: params[:id]) 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 present paginate(user.keys), with: Entities::SSHKey
end end
......
...@@ -13,6 +13,7 @@ module Banzai ...@@ -13,6 +13,7 @@ module Banzai
# Returns a Project, or nil if the reference can't be found # Returns a Project, or nil if the reference can't be found
def parent_from_ref(ref) def parent_from_ref(ref)
return context[:project] || context[:group] unless ref return context[:project] || context[:group] unless ref
return context[:project] if context[:project]&.full_path == ref
Project.find_by_full_path(ref) Project.find_by_full_path(ref)
end end
......
...@@ -48,7 +48,7 @@ module Banzai ...@@ -48,7 +48,7 @@ module Banzai
include_ancestor_groups: true, include_ancestor_groups: true,
only_group_labels: true } only_group_labels: true }
else else
{ project_id: parent.id, { project: parent,
include_ancestor_groups: true } include_ancestor_groups: true }
end end
......
# 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
...@@ -25,17 +25,19 @@ module Gitlab ...@@ -25,17 +25,19 @@ module Gitlab
include Entry::Validatable include Entry::Validatable
include Entry::Attributable include Entry::Attributable
attributes :refs, :kubernetes, :variables ALLOWED_KEYS = %i[refs kubernetes variables changes].freeze
attributes :refs, :kubernetes, :variables, :changes
validations do validations do
validates :config, presence: true validates :config, presence: true
validates :config, allowed_keys: %i[refs kubernetes variables] validates :config, allowed_keys: ALLOWED_KEYS
validate :variables_expressions_syntax validate :variables_expressions_syntax
with_options allow_nil: true do with_options allow_nil: true do
validates :refs, array_of_strings_or_regexps: true validates :refs, array_of_strings_or_regexps: true
validates :kubernetes, allowed_values: %w[active] validates :kubernetes, allowed_values: %w[active]
validates :variables, array_of_strings: true validates :variables, array_of_strings: true
validates :changes, array_of_strings: true
end end
def variables_expressions_syntax def variables_expressions_syntax
......
...@@ -689,9 +689,6 @@ rollout 100%: ...@@ -689,9 +689,6 @@ rollout 100%:
helm version --client helm version --client
tiller -version 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" 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 chmod +x /usr/bin/kubectl
kubectl version --client kubectl version --client
...@@ -800,9 +797,9 @@ rollout 100%: ...@@ -800,9 +797,9 @@ rollout 100%:
function initialize_tiller() { function initialize_tiller() {
echo "Checking Tiller..." echo "Checking Tiller..."
helm local start
helm local status
export HELM_HOST=":44134" 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 if ! helm version --debug; then
echo "Failed to init Tiller." echo "Failed to init Tiller."
......
...@@ -18,6 +18,10 @@ module Gitlab ...@@ -18,6 +18,10 @@ module Gitlab
indexed_by_path[path] indexed_by_path[path]
end end
def paths
@collection.map(&:path)
end
private private
def indexed_by_path def indexed_by_path
......
# 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
...@@ -220,7 +220,7 @@ module Gitlab ...@@ -220,7 +220,7 @@ module Gitlab
result result
end end
SERVER_FEATURE_FLAGS = %w[gogit_findcommit git_v2].freeze SERVER_FEATURE_FLAGS = %w[gogit_findcommit].freeze
def self.server_feature_flags def self.server_feature_flags
SERVER_FEATURE_FLAGS.map do |f| SERVER_FEATURE_FLAGS.map do |f|
......
...@@ -257,6 +257,9 @@ module QA ...@@ -257,6 +257,9 @@ module QA
autoload :Dropzone, 'qa/page/component/dropzone' autoload :Dropzone, 'qa/page/component/dropzone'
autoload :GroupsFilter, 'qa/page/component/groups_filter' autoload :GroupsFilter, 'qa/page/component/groups_filter'
autoload :Select2, 'qa/page/component/select2' autoload :Select2, 'qa/page/component/select2'
module Issuable
autoload :Common, 'qa/page/component/issuable/common'
end
end end
end end
......
# 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
...@@ -5,6 +5,8 @@ module QA ...@@ -5,6 +5,8 @@ module QA
module Project module Project
module Issue module Issue
class Show < Page::Base class Show < Page::Base
include Page::Component::Issuable::Common
view 'app/views/projects/issues/show.html.haml' do view 'app/views/projects/issues/show.html.haml' do
element :issue_details, '.issue-details' element :issue_details, '.issue-details'
element :title, '.title' element :title, '.title'
......
...@@ -202,8 +202,8 @@ describe GroupsController do ...@@ -202,8 +202,8 @@ describe GroupsController do
end end
describe 'GET #issues' do describe 'GET #issues' do
let(:issue_1) { create(:issue, project: project) } let(:issue_1) { create(:issue, project: project, title: 'foo') }
let(:issue_2) { create(:issue, project: project) } let(:issue_2) { create(:issue, project: project, title: 'bar') }
before do before do
create_list(:award_emoji, 3, awardable: issue_2) create_list(:award_emoji, 3, awardable: issue_2)
...@@ -224,6 +224,31 @@ describe GroupsController do ...@@ -224,6 +224,31 @@ describe GroupsController do
expect(assigns(:issues)).to eq [issue_2, issue_1] expect(assigns(:issues)).to eq [issue_2, issue_1]
end end
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 end
describe 'GET #merge_requests' do describe 'GET #merge_requests' do
......
...@@ -637,6 +637,18 @@ describe Projects::IssuesController do ...@@ -637,6 +637,18 @@ describe Projects::IssuesController do
project_id: project, project_id: project,
id: id id: id
end 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 end
describe 'GET #realtime_changes' do describe 'GET #realtime_changes' do
......
...@@ -225,7 +225,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -225,7 +225,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to match_schema('job/job_details') expect(json_response).to match_schema('job/job_details')
expect(json_response['deployment_status']["status"]).to eq 'creating' 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 expect(json_response['deployment_status']["environment"]).not_to be_nil
end end
end end
......
require_relative '../support/helpers/test_env' require_relative '../support/helpers/test_env'
FactoryBot.define do FactoryBot.define do
PAGES_ACCESS_LEVEL_SCHEMA_VERSION = 20180423204600
# Project without repository # Project without repository
# #
# Project does not have bare repository. # Project does not have bare repository.
...@@ -23,6 +25,7 @@ FactoryBot.define do ...@@ -23,6 +25,7 @@ FactoryBot.define do
issues_access_level ProjectFeature::ENABLED issues_access_level ProjectFeature::ENABLED
merge_requests_access_level ProjectFeature::ENABLED merge_requests_access_level ProjectFeature::ENABLED
repository_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 # we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first # `#ci_cd_settings` relation needs to be created first
...@@ -34,13 +37,20 @@ FactoryBot.define do ...@@ -34,13 +37,20 @@ FactoryBot.define do
builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min 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 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, wiki_access_level: evaluator.wiki_access_level,
builds_access_level: builds_access_level, builds_access_level: builds_access_level,
snippets_access_level: evaluator.snippets_access_level, snippets_access_level: evaluator.snippets_access_level,
issues_access_level: evaluator.issues_access_level, issues_access_level: evaluator.issues_access_level,
merge_requests_access_level: merge_requests_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 # Normally the class Projects::CreateService is used for creating
# projects, and this class takes care of making sure the owner and current # projects, and this class takes care of making sure the owner and current
...@@ -244,6 +254,10 @@ FactoryBot.define do ...@@ -244,6 +254,10 @@ FactoryBot.define do
trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED } trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED } trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE } 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 trait :auto_devops do
association :auto_devops, factory: :project_auto_devops association :auto_devops, factory: :project_auto_devops
......
...@@ -5,6 +5,7 @@ describe 'GFM autocomplete', :js do ...@@ -5,6 +5,7 @@ describe 'GFM autocomplete', :js do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') } let(:label) { create(:label, project: project, title: 'special+') }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let!(:project_snippet) { create(:project_snippet, project: project, title: 'code snippet') }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
...@@ -301,6 +302,16 @@ describe 'GFM autocomplete', :js do ...@@ -301,6 +302,16 @@ describe 'GFM autocomplete', :js do
end end
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 private
def expect_to_wrap(should_wrap, item, note, value) def expect_to_wrap(should_wrap, item, note, value)
......
...@@ -125,6 +125,14 @@ describe IssuesFinder do ...@@ -125,6 +125,14 @@ describe IssuesFinder do
end end
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 context 'filtering by upcoming milestone' do
let(:params) { { milestone_title: Milestone::Upcoming.name } } let(:params) { { milestone_title: Milestone::Upcoming.name } }
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
"type": "object", "type": "object",
"required": [ "required": [
"status", "status",
"icon",
"environment" "environment"
], ],
"properties": { "properties": {
...@@ -20,7 +19,6 @@ ...@@ -20,7 +19,6 @@
{ "type": "null" } { "type": "null" }
] ]
}, },
"icon": { "type": "string" },
"environment": { "$ref": "../environment.json" } "environment": { "$ref": "../environment.json" }
}, },
"additionalProperties": false "additionalProperties": false
......
...@@ -10,7 +10,7 @@ describe Types::PermissionTypes::Project do ...@@ -10,7 +10,7 @@ describe Types::PermissionTypes::Project do
:read_commit_status, :request_access, :create_pipeline, :create_pipeline_schedule, :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, :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, :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) expect(described_class).to have_graphql_fields(expected_permissions)
......
...@@ -174,9 +174,7 @@ describe ApplicationHelper do ...@@ -174,9 +174,7 @@ describe ApplicationHelper do
it 'returns paths for autocomplete_sources_controller' do it 'returns paths for autocomplete_sources_controller' do
sources = helper.autocomplete_data_sources(project, noteable_type) sources = helper.autocomplete_data_sources(project, noteable_type)
expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets])
expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands])
sources.keys.each do |key| sources.keys.each do |key|
expect(sources[key]).not_to be_nil expect(sources[key]).not_to be_nil
end end
......
...@@ -103,7 +103,7 @@ describe('GfmAutoComplete', function () { ...@@ -103,7 +103,7 @@ describe('GfmAutoComplete', function () {
gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext) gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext)
); );
const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%']; const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%', '$'];
const otherFlags = ['/', ':']; const otherFlags = ['/', ':'];
const flags = flagsUseDefaultMatcher.concat(otherFlags); const flags = flagsUseDefaultMatcher.concat(otherFlags);
......
require 'spec_helper' require 'spec_helper'
describe Banzai::CrossProjectReference do 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 describe '#parent_from_ref' do
context 'when no project was referenced' do context 'when no project was referenced' do
it 'returns the project from context' do it 'returns the project from context' do
project = double 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
end end
...@@ -18,15 +23,15 @@ describe Banzai::CrossProjectReference do ...@@ -18,15 +23,15 @@ describe Banzai::CrossProjectReference do
it 'returns the group from context' do it 'returns the group from context' do
group = double 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
end end
context 'when referenced project does not exist' do context 'when referenced project does not exist' do
it 'returns nil' 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
end end
...@@ -37,7 +42,7 @@ describe Banzai::CrossProjectReference do ...@@ -37,7 +42,7 @@ describe Banzai::CrossProjectReference do
expect(Project).to receive(:find_by_full_path) expect(Project).to receive(:find_by_full_path)
.with('cross/reference').and_return(project2) .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 end
end end
......
...@@ -60,6 +60,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter do ...@@ -60,6 +60,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter do
exp = act = "See #{commit1.id.reverse}...#{commit2.id}" 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(commit1.id.reverse)
allow(project.repository).to receive(:commit).with(commit2.id)
expect(reference_filter(act).to_html).to eq exp expect(reference_filter(act).to_html).to eq exp
end end
......
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
require 'spec_helper' require 'fast_spec_helper'
require_dependency 'active_model'
describe Gitlab::Ci::Config::Entry::Policy do describe Gitlab::Ci::Config::Entry::Policy do
let(:entry) { described_class.new(config) } let(:entry) { described_class.new(config) }
...@@ -124,6 +125,23 @@ describe Gitlab::Ci::Config::Entry::Policy do ...@@ -124,6 +125,23 @@ describe Gitlab::Ci::Config::Entry::Policy do
end end
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 context 'when specifying unknown policy' do
let(:config) { { refs: ['master'], invalid: :something } } let(:config) { { refs: ['master'], invalid: :something } }
......
...@@ -1369,7 +1369,7 @@ module Gitlab ...@@ -1369,7 +1369,7 @@ module Gitlab
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
end 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'] } } }) config = YAML.dump({ rspec: { script: 'test', only: { variables: ['== null'] } } })
expect { Gitlab::Ci::YamlProcessor.new(config) } expect { Gitlab::Ci::YamlProcessor.new(config) }
...@@ -1377,6 +1377,14 @@ module Gitlab ...@@ -1377,6 +1377,14 @@ module Gitlab
'jobs:rspec:only variables invalid expression syntax') 'jobs:rspec:only variables invalid expression syntax')
end 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 it 'returns errors if extended hash configuration is invalid' do
config = YAML.dump({ rspec: { extends: 'something', script: 'test' } }) config = YAML.dump({ rspec: { extends: 'something', script: 'test' } })
......
...@@ -14,7 +14,7 @@ describe Gitlab::Git::DiffStatsCollection do ...@@ -14,7 +14,7 @@ describe Gitlab::Git::DiffStatsCollection do
let(:diff_stats) { [stats_a, stats_b] } let(:diff_stats) { [stats_a, stats_b] }
let(:collection) { described_class.new(diff_stats) } 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 it 'returns stats by path when found' do
expect(collection.find_by_path('foo')).to eq(stats_a) expect(collection.find_by_path('foo')).to eq(stats_a)
end end
...@@ -23,4 +23,10 @@ describe Gitlab::Git::DiffStatsCollection do ...@@ -23,4 +23,10 @@ describe Gitlab::Git::DiffStatsCollection do
expect(collection.find_by_path('no-file')).to be_nil expect(collection.find_by_path('no-file')).to be_nil
end end
end end
describe '#paths' do
it 'returns only modified paths' do
expect(collection.paths).to eq %w[foo bar]
end
end
end end
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
...@@ -493,6 +493,7 @@ ProjectFeature: ...@@ -493,6 +493,7 @@ ProjectFeature:
- snippets_access_level - snippets_access_level
- builds_access_level - builds_access_level
- repository_access_level - repository_access_level
- pages_access_level
- created_at - created_at
- updated_at - updated_at
ProtectedBranch::MergeAccessLevel: ProtectedBranch::MergeAccessLevel:
......
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
...@@ -837,6 +837,57 @@ describe Ci::Pipeline, :mailer do ...@@ -837,6 +837,57 @@ describe Ci::Pipeline, :mailer do
end end
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 describe '#has_kubernetes_active?' do
context 'when kubernetes is active' do context 'when kubernetes is active' do
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
......
...@@ -65,7 +65,8 @@ describe InternalId do ...@@ -65,7 +65,8 @@ describe InternalId do
context 'with an insufficient schema version' do context 'with an insufficient schema version' do
before do before do
described_class.reset_column_information 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 end
let(:init) { double('block') } let(:init) { double('block') }
......
...@@ -17,7 +17,7 @@ describe ProjectFeature do ...@@ -17,7 +17,7 @@ describe ProjectFeature do
end end
describe '#feature_available?' do 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 context 'when features are disabled' do
it "returns false" do it "returns false" do
...@@ -73,6 +73,22 @@ describe ProjectFeature do ...@@ -73,6 +73,22 @@ describe ProjectFeature do
end end
end 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 end
context 'repository related features' do context 'repository related features' do
...@@ -96,6 +112,19 @@ describe ProjectFeature do ...@@ -96,6 +112,19 @@ describe ProjectFeature do
end end
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 describe '#*_enabled?' do
let(:features) { %w(wiki builds merge_requests) } let(:features) { %w(wiki builds merge_requests) }
......
...@@ -56,6 +56,7 @@ describe API::Issues do ...@@ -56,6 +56,7 @@ describe API::Issues do
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { URI.escape(Milestone::None.title) } let(:no_milestone_title) { URI.escape(Milestone::None.title) }
let(:any_milestone_title) { URI.escape(Milestone::Any.title) }
before(:all) do before(:all) do
project.add_reporter(user) project.add_reporter(user)
...@@ -811,6 +812,15 @@ describe API::Issues do ...@@ -811,6 +812,15 @@ describe API::Issues do
expect(json_response.first['id']).to eq(confidential_issue.id) expect(json_response.first['id']).to eq(confidential_issue.id)
end 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 it 'sorts by created_at descending by default' do
get api("#{base_url}/issues", user) get api("#{base_url}/issues", user)
......
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
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
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
...@@ -785,35 +785,25 @@ describe API::Users do ...@@ -785,35 +785,25 @@ describe API::Users do
end end
describe 'GET /user/:id/keys' do describe 'GET /user/:id/keys' do
before do it 'returns 404 for non-existing user' do
admin user_id = not_existing_user_id
end
context 'when unauthenticated' do get api("/users/#{user_id}/keys")
it 'returns authentication error' do
get api("/users/#{user.id}/keys")
expect(response).to have_gitlab_http_status(401)
end
end
context 'when authenticated' do expect(response).to have_gitlab_http_status(404)
it 'returns 404 for non-existing user' do expect(json_response['message']).to eq('404 User Not Found')
get api('/users/999999/keys', admin) 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 it 'returns array of ssh keys' do
user.keys << key user.keys << key
user.save 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 have_gitlab_http_status(200)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(key.title) expect(json_response.first['title']).to eq(key.title)
end
end end
end end
......
...@@ -133,8 +133,9 @@ describe 'project routing' do ...@@ -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 # 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 # 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 # 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 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 it "to ##{action}" do
expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq') expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
end end
......
...@@ -340,6 +340,27 @@ describe Projects::UpdateService do ...@@ -340,6 +340,27 @@ describe Projects::UpdateService do
call_service call_service
end end
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 end
describe '#run_auto_devops_pipeline?' do describe '#run_auto_devops_pipeline?' do
......
...@@ -34,6 +34,11 @@ Rainbow.enabled = false ...@@ -34,6 +34,11 @@ Rainbow.enabled = false
# Requires supporting ruby files with custom matchers and macros, etc, # Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories. # in spec/support/ and its subdirectories.
# Requires helpers, and shared contexts/examples first since they're used in other support files # 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/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_contexts/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/support/shared_examples/*.rb")].each { |f| require f } Dir[Rails.root.join("spec/support/shared_examples/*.rb")].each { |f| require f }
......
...@@ -616,14 +616,10 @@ ...@@ -616,14 +616,10 @@
lodash "^4.17.10" lodash "^4.17.10"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@gitlab-org/gitlab-svgs@^1.23.0": "@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.29.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":
version "1.31.0" version "1.31.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.31.0.tgz#495b074669f93af40e34f9978ce887773dea470a" 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": "@gitlab-org/gitlab-ui@^1.8.0":
version "1.8.0" version "1.8.0"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment