Commit 2b6b7b2e authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-10-02

# Conflicts:
#	db/schema.rb
#	locale/gitlab.pot
#	qa/qa/page/menu/admin.rb

[ci skip]
parents e672c821 eda6b43c
6.1.1 7.0.0
\ No newline at end of file
/**
* Highlights the current user in existing elements with a user ID data attribute.
*
* @param elements DOM elements that represent user mentions
*/
export default function highlightCurrentUser(elements) {
const currentUserId = gon && gon.current_user_id;
if (!currentUserId) {
return;
}
elements.forEach(element => {
if (parseInt(element.dataset.user, 10) === currentUserId) {
element.classList.add('current-user');
}
});
}
...@@ -2,6 +2,7 @@ import $ from 'jquery'; ...@@ -2,6 +2,7 @@ import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight'; import syntaxHighlight from '~/syntax_highlight';
import renderMath from './render_math'; import renderMath from './render_math';
import renderMermaid from './render_mermaid'; import renderMermaid from './render_mermaid';
import highlightCurrentUser from './highlight_current_user';
// Render GitLab flavoured Markdown // Render GitLab flavoured Markdown
// //
...@@ -11,6 +12,7 @@ $.fn.renderGFM = function renderGFM() { ...@@ -11,6 +12,7 @@ $.fn.renderGFM = function renderGFM() {
syntaxHighlight(this.find('.js-syntax-highlight')); syntaxHighlight(this.find('.js-syntax-highlight'));
renderMath(this.find('.js-render-math')); renderMath(this.find('.js-render-math'));
renderMermaid(this.find('.js-render-mermaid')); renderMermaid(this.find('.js-render-mermaid'));
highlightCurrentUser(this.find('.gfm-project_member').get());
return this; return this;
}; };
......
...@@ -31,11 +31,17 @@ function blockTagText(text, textArea, blockTag, selected) { ...@@ -31,11 +31,17 @@ function blockTagText(text, textArea, blockTag, selected) {
} }
} }
function moveCursor(textArea, tag, wrapped, removedLastNewLine) { function moveCursor({ textArea, tag, wrapped, removedLastNewLine, select }) {
var pos; var pos;
if (!textArea.setSelectionRange) { if (!textArea.setSelectionRange) {
return; return;
} }
if (select && select.length > 0) {
// calculate the part of the text to be selected
const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select));
const endPosition = startPosition + select.length;
return textArea.setSelectionRange(startPosition, endPosition);
}
if (textArea.selectionStart === textArea.selectionEnd) { if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) { if (wrapped) {
pos = textArea.selectionStart - tag.length; pos = textArea.selectionStart - tag.length;
...@@ -51,7 +57,7 @@ function moveCursor(textArea, tag, wrapped, removedLastNewLine) { ...@@ -51,7 +57,7 @@ function moveCursor(textArea, tag, wrapped, removedLastNewLine) {
} }
} }
export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap) { export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select }) {
var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false; removedLastNewLine = false;
removedFirstNewLine = false; removedFirstNewLine = false;
...@@ -82,11 +88,16 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap ...@@ -82,11 +88,16 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
const textPlaceholder = '{text}';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') { if (blockTag != null && blockTag !== '') {
textToInsert = blockTagText(text, textArea, blockTag, selected); textToInsert = blockTagText(text, textArea, blockTag, selected);
} else { } else {
textToInsert = selectedSplit.map(function(val) { textToInsert = selectedSplit.map(function(val) {
if (tag.indexOf(textPlaceholder) > -1) {
return tag.replace(textPlaceholder, val);
}
if (val.indexOf(tag) === 0) { if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, '')); return "" + (val.replace(tag, ''));
} else { } else {
...@@ -94,6 +105,8 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap ...@@ -94,6 +105,8 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
} }
}).join('\n'); }).join('\n');
} }
} else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, selected);
} else { } else {
textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' '); textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' ');
} }
...@@ -107,17 +120,17 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap ...@@ -107,17 +120,17 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
} }
insertText(textArea, textToInsert); insertText(textArea, textToInsert);
return moveCursor(textArea, tag, wrap, removedLastNewLine); return moveCursor({ textArea, tag: tag.replace(textPlaceholder, selected), wrap, removedLastNewLine, select });
} }
function updateText(textArea, tag, blockTag, wrap) { function updateText({ textArea, tag, blockTag, wrap, select }) {
var $textArea, selected, text; var $textArea, selected, text;
$textArea = $(textArea); $textArea = $(textArea);
textArea = $textArea.get(0); textArea = $textArea.get(0);
text = $textArea.val(); text = $textArea.val();
selected = selectedText(text, textArea); selected = selectedText(text, textArea);
$textArea.focus(); $textArea.focus();
return insertMarkdownText(textArea, text, tag, blockTag, selected, wrap); return insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select });
} }
function replaceRange(s, start, end, substitute) { function replaceRange(s, start, end, substitute) {
...@@ -127,7 +140,12 @@ function replaceRange(s, start, end, substitute) { ...@@ -127,7 +140,12 @@ function replaceRange(s, start, end, substitute) {
export function addMarkdownListeners(form) { export function addMarkdownListeners(form) {
return $('.js-md', form).off('click').on('click', function() { return $('.js-md', form).off('click').on('click', function() {
const $this = $(this); const $this = $(this);
return updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend')); return updateText({
textArea: $this.closest('.md-area').find('textarea'),
tag: $this.data('mdTag'),
blockTag: $this.data('mdBlock'),
wrap: !$this.data('mdPrepend'),
select: $this.data('mdSelect') });
}); });
} }
......
...@@ -11,6 +11,7 @@ import commentForm from './comment_form.vue'; ...@@ -11,6 +11,7 @@ import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
export default { export default {
name: 'NotesApp', name: 'NotesApp',
...@@ -96,6 +97,9 @@ export default { ...@@ -96,6 +97,9 @@ export default {
}); });
} }
}, },
updated() {
this.$nextTick(() => highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member')));
},
methods: { methods: {
...mapActions({ ...mapActions({
fetchDiscussions: 'fetchDiscussions', fetchDiscussions: 'fetchDiscussions',
......
...@@ -105,6 +105,12 @@ ...@@ -105,6 +105,12 @@
button-title="Insert code" button-title="Insert code"
icon="code" icon="code"
/> />
<toolbar-button
tag="[{text}](url)"
tag-select="url"
button-title="Add a link"
icon="link"
/>
<toolbar-button <toolbar-button
:prepend="true" :prepend="true"
tag="* " tag="* "
......
...@@ -27,6 +27,11 @@ ...@@ -27,6 +27,11 @@
required: false, required: false,
default: '', default: '',
}, },
tagSelect: {
type: String,
required: false,
default: '',
},
prepend: { prepend: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -40,6 +45,7 @@ ...@@ -40,6 +45,7 @@
<button <button
v-tooltip v-tooltip
:data-md-tag="tag" :data-md-tag="tag"
:data-md-select="tagSelect"
:data-md-block="tagBlock" :data-md-block="tagBlock"
:data-md-prepend="prepend" :data-md-prepend="prepend"
:title="buttonTitle" :title="buttonTitle"
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
padding: 0 2px; padding: 0 2px;
background-color: $blue-100; background-color: $blue-100;
border-radius: $border-radius-default; border-radius: $border-radius-default;
&.current-user {
background-color: $orange-100;
}
} }
.gfm-color_chip { .gfm-color_chip {
......
...@@ -14,6 +14,8 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -14,6 +14,8 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :entry, only: [:file] before_action :entry, only: [:file]
def download def download
return render_404 unless artifacts_file
send_upload(artifacts_file, attachment: artifacts_file.filename) send_upload(artifacts_file, attachment: artifacts_file.filename)
end end
...@@ -100,7 +102,7 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -100,7 +102,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def artifacts_file def artifacts_file
@artifacts_file ||= build.artifacts_file @artifacts_file ||= build.artifacts_file_for_type(params[:file_type] || :archive)
end end
def entry def entry
......
...@@ -255,7 +255,8 @@ module ApplicationSettingsHelper ...@@ -255,7 +255,8 @@ module ApplicationSettingsHelper
:user_default_internal_regex, :user_default_internal_regex,
:user_oauth_applications, :user_oauth_applications,
:version_check_enabled, :version_check_enabled,
:web_ide_clientside_preview_enabled :web_ide_clientside_preview_enabled,
:diff_max_patch_bytes
] ]
end end
......
...@@ -329,11 +329,15 @@ module IssuablesHelper ...@@ -329,11 +329,15 @@ module IssuablesHelper
end end
def issuable_button_visibility(issuable, closed) def issuable_button_visibility(issuable, closed)
return 'hidden' if issuable_button_hidden?(issuable, closed)
end
def issuable_button_hidden?(issuable, closed)
case issuable case issuable
when Issue when Issue
issue_button_visibility(issuable, closed) issue_button_hidden?(issuable, closed)
when MergeRequest when MergeRequest
merge_request_button_visibility(issuable, closed) merge_request_button_hidden?(issuable, closed)
end end
end end
......
...@@ -66,7 +66,11 @@ module IssuesHelper ...@@ -66,7 +66,11 @@ module IssuesHelper
end end
def issue_button_visibility(issue, closed) def issue_button_visibility(issue, closed)
return 'hidden' if issue.closed? == closed return 'hidden' if issue_button_hidden?(issue, closed)
end
def issue_button_hidden?(issue, closed)
issue.closed? == closed || (!closed && issue.discussion_locked)
end end
def confidential_icon(issue) def confidential_icon(issue)
......
...@@ -82,7 +82,11 @@ module MergeRequestsHelper ...@@ -82,7 +82,11 @@ module MergeRequestsHelper
end end
def merge_request_button_visibility(merge_request, closed) def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? return 'hidden' if merge_request_button_hidden?(merge_request, closed)
end
def merge_request_button_hidden?(merge_request, closed)
merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end end
def merge_request_version_path(project, merge_request, merge_request_diff, start_sha = nil) def merge_request_version_path(project, merge_request, merge_request_diff, start_sha = nil)
......
...@@ -183,6 +183,12 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -183,6 +183,12 @@ class ApplicationSetting < ActiveRecord::Base
numericality: { less_than_or_equal_to: :gitaly_timeout_default }, numericality: { less_than_or_equal_to: :gitaly_timeout_default },
if: :gitaly_timeout_default if: :gitaly_timeout_default
validates :diff_max_patch_bytes,
presence: true,
numericality: { only_integer: true,
greater_than_or_equal_to: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
less_than_or_equal_to: Gitlab::Git::Diff::MAX_PATCH_BYTES_UPPER_BOUND }
validates :user_default_internal_regex, js_regex: true, allow_nil: true validates :user_default_internal_regex, js_regex: true, allow_nil: true
SUPPORTED_KEY_TYPES.each do |type| SUPPORTED_KEY_TYPES.each do |type|
...@@ -294,7 +300,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -294,7 +300,8 @@ class ApplicationSetting < ActiveRecord::Base
user_default_external: false, user_default_external: false,
user_default_internal_regex: nil, user_default_internal_regex: nil,
user_show_add_ssh_key_message: true, user_show_add_ssh_key_message: true,
usage_stats_set_by_user_id: nil usage_stats_set_by_user_id: nil,
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES
} }
end end
......
...@@ -526,6 +526,13 @@ module Ci ...@@ -526,6 +526,13 @@ module Ci
self.job_artifacts.update_all(expire_at: nil) self.job_artifacts.update_all(expire_at: nil)
end end
def artifacts_file_for_type(type)
file = job_artifacts.find_by(file_type: Ci::JobArtifact.file_types[type])&.file
# TODO: to be removed once legacy artifacts is removed
file ||= legacy_artifacts_file if type == :archive
file
end
def coverage_regex def coverage_regex
super || project.try(:build_coverage_regex) super || project.try(:build_coverage_regex)
end end
......
...@@ -17,6 +17,7 @@ module Ci ...@@ -17,6 +17,7 @@ module Ci
metadata: nil, metadata: nil,
trace: nil, trace: nil,
junit: 'junit.xml', junit: 'junit.xml',
codequality: 'codequality.json',
sast: 'gl-sast-report.json', sast: 'gl-sast-report.json',
dependency_scanning: 'gl-dependency-scanning-report.json', dependency_scanning: 'gl-dependency-scanning-report.json',
container_scanning: 'gl-container-scanning-report.json', container_scanning: 'gl-container-scanning-report.json',
...@@ -28,6 +29,7 @@ module Ci ...@@ -28,6 +29,7 @@ module Ci
metadata: :gzip, metadata: :gzip,
trace: :raw, trace: :raw,
junit: :gzip, junit: :gzip,
codequality: :gzip,
sast: :gzip, sast: :gzip,
dependency_scanning: :gzip, dependency_scanning: :gzip,
container_scanning: :gzip, container_scanning: :gzip,
...@@ -76,7 +78,8 @@ module Ci ...@@ -76,7 +78,8 @@ module Ci
sast: 5, ## EE-specific sast: 5, ## EE-specific
dependency_scanning: 6, ## EE-specific dependency_scanning: 6, ## EE-specific
container_scanning: 7, ## EE-specific container_scanning: 7, ## EE-specific
dast: 8 ## EE-specific dast: 8, ## EE-specific
codequality: 9 ## EE-specific
} }
enum file_format: { enum file_format: {
......
...@@ -5,8 +5,10 @@ module Storage ...@@ -5,8 +5,10 @@ module Storage
extend ActiveSupport::Concern extend ActiveSupport::Concern
def move_dir def move_dir
if any_project_has_container_registry_tags? proj_with_tags = first_project_with_container_registry_tags
raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
if proj_with_tags
raise Gitlab::UpdatePathError.new("Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry")
end end
parent_was = if parent_changed? && parent_id_was.present? parent_was = if parent_changed? && parent_id_was.present?
......
...@@ -136,6 +136,10 @@ class Namespace < ActiveRecord::Base ...@@ -136,6 +136,10 @@ class Namespace < ActiveRecord::Base
all_projects.any?(&:has_container_registry_tags?) all_projects.any?(&:has_container_registry_tags?)
end end
def first_project_with_container_registry_tags
all_projects.find(&:has_container_registry_tags?)
end
def send_update_instructions def send_update_instructions
projects.each do |project| projects.each do |project|
project.send_move_instructions("#{full_path_was}/#{project.path}") project.send_move_instructions("#{full_path_was}/#{project.path}")
......
...@@ -1381,6 +1381,18 @@ class Project < ActiveRecord::Base ...@@ -1381,6 +1381,18 @@ class Project < ActiveRecord::Base
end end
end end
# Filters `users` to return only authorized users of the project
def members_among(users)
if users.is_a?(ActiveRecord::Relation) && !users.loaded?
authorized_users.merge(users)
else
return [] if users.empty?
user_ids = authorized_users.where(users: { id: users.map(&:id) }).pluck(:id)
users.select { |user| user_ids.include?(user.id) }
end
end
def default_branch def default_branch
@default_branch ||= repository.root_ref if repository.exists? @default_branch ||= repository.root_ref if repository.exists?
end end
......
= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-merge-request-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :diff_max_patch_bytes, 'Maximum diff patch size (Bytes)', class: 'label-light'
= f.number_field :diff_max_patch_bytes, class: 'form-control'
%span.form-text.text-muted
Diff files surpassing this limit will be presented as 'too large'
and won't be expandable.
= link_to icon('question-circle'),
help_page_path('user/admin_area/diff_limits',
anchor: 'maximum-diff-patch-size')
= f.submit _('Save changes'), class: 'btn btn-success'
...@@ -24,6 +24,17 @@ ...@@ -24,6 +24,17 @@
.settings-content .settings-content
= render 'account_and_limit' = render 'account_and_limit'
%section.settings.as-diff-limits.no-animate#js-merge-request-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Diff limits')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Diff content limits')
.settings-content
= render 'diff_limits'
%section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded_by_default?) } %section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header .settings-header
%h4 %h4
......
...@@ -18,14 +18,15 @@ ...@@ -18,14 +18,15 @@
Preview Preview
%li.md-header-toolbar.active %li.md-header-toolbar.active
= markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" }) = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: s_("MarkdownToolbar|Add bold text") })
= markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" }) = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: s_("MarkdownToolbar|Add italic text") })
= markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: s_("MarkdownToolbar|Insert a quote") })
= markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: s_("MarkdownToolbar|Insert code") })
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) = markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: s_("MarkdownToolbar|Add a link") })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a bullet list") })
= markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a numbered list") })
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: "Go full screen", data: { container: "body" } } = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a task list") })
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: s_("MarkdownToolbar|Go full screen"), data: { container: "body" } }
= sprite_icon("screen-full") = sprite_icon("screen-full")
.md-write-holder .md-write-holder
......
.group-empty-state.row.align-items-center.justify-content-center.qa-groups-empty-state .group-empty-state.row.align-items-center.justify-content-center
.icon.text-center.order-md-2 .icon.text-center.order-md-2
= custom_icon("icon_empty_groups") = custom_icon("icon_empty_groups")
......
= form_tag request.path, method: :get, class: "group-filter-form js-group-filter-form", id: 'group-filter-form' do |f| = form_tag request.path, method: :get, class: "group-filter-form js-group-filter-form", id: 'group-filter-form' do |f|
= search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2" = search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter qa-groups-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
- is_current_user = issuable_author_is_current_user(issuable) - is_current_user = issuable_author_is_current_user(issuable)
- display_issuable_type = issuable_display_type(issuable) - display_issuable_type = issuable_display_type(issuable)
- button_method = issuable_close_reopen_button_method(issuable) - button_method = issuable_close_reopen_button_method(issuable)
- are_close_and_open_buttons_hidden = issuable_button_hidden?(issuable, true) && issuable_button_hidden?(issuable, false)
- if can_update - if is_current_user
- if is_current_user - if can_update
= link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method, = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
- else - if can_reopen
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
- if can_reopen && is_current_user
= link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method, = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- else - else
- if can_update && !are_close_and_open_buttons_hidden
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
- else
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse' class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse'
---
title: Add link button to markdown editor toolbar
merge_request: 18579
author: Jan Beckmann
type: added
---
title: Hides Close Merge request btn on merged Merge request
merge_request: 21840
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Fix migration to avoid an exception during upgrade
merge_request: 22055
author:
type: fixed
---
title: Use local tiller for Auto DevOps
merge_request: 22036
author:
type: changed
---
title: Show SHA for pre-release versions on the help page
merge_request: 22026
author:
type: changed
---
title: Make single diff patch limit configurable
merge_request: 21886
author:
type: added
---
title: Improve logging when username update fails due to registry tags
merge_request: 22038
author:
type: other
---
title: Highlight current user in comments
merge_request: 21406
author:
type: changed
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddDiffMaxPatchBytesToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:application_settings,
:diff_max_patch_bytes,
:integer,
default: 100.kilobytes,
allow_null: false)
end
def down
remove_column(:application_settings, :diff_max_patch_bytes)
end
end
...@@ -5,6 +5,8 @@ class RenameLoginRootNamespaces < ActiveRecord::Migration ...@@ -5,6 +5,8 @@ class RenameLoginRootNamespaces < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
disable_ddl_transaction!
# We're taking over the /login namespace as part of a fix for the Jira integration # We're taking over the /login namespace as part of a fix for the Jira integration
def up def up
disable_statement_timeout do disable_statement_timeout do
......
...@@ -11,7 +11,11 @@ ...@@ -11,7 +11,11 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
<<<<<<< HEAD
ActiveRecord::Schema.define(version: 20180924070647) do ActiveRecord::Schema.define(version: 20180924070647) do
=======
ActiveRecord::Schema.define(version: 20180924141949) do
>>>>>>> upstream/master
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -217,6 +221,7 @@ ActiveRecord::Schema.define(version: 20180924070647) do ...@@ -217,6 +221,7 @@ ActiveRecord::Schema.define(version: 20180924070647) do
t.integer "custom_project_templates_group_id" t.integer "custom_project_templates_group_id"
t.integer "usage_stats_set_by_user_id" t.integer "usage_stats_set_by_user_id"
t.integer "receive_max_input_size" t.integer "receive_max_input_size"
t.integer "diff_max_patch_bytes", default: 102400, null: false
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
......
...@@ -56,6 +56,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. ...@@ -56,6 +56,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Enforcing Terms of Service](../user/admin_area/settings/terms.md) - [Enforcing Terms of Service](../user/admin_area/settings/terms.md)
- [Third party offers](../user/admin_area/settings/third_party_offers.md) - [Third party offers](../user/admin_area/settings/third_party_offers.md)
- [Compliance](compliance.md): A collection of features from across the application that you may configure to help ensure that your GitLab instance and DevOps workflow meet compliance standards. - [Compliance](compliance.md): A collection of features from across the application that you may configure to help ensure that your GitLab instance and DevOps workflow meet compliance standards.
- [Diff limits](../user/admin_area/diff_limits.md): Configure the diff rendering size limits of branch comparison pages.
#### Customizing GitLab's appearance #### Customizing GitLab's appearance
......
...@@ -2,13 +2,10 @@ ...@@ -2,13 +2,10 @@
Currently we rely on different sources to present diffs, these include: Currently we rely on different sources to present diffs, these include:
- Rugged gem
- Gitaly service - Gitaly service
- Database (through `merge_request_diff_files`) - Database (through `merge_request_diff_files`)
- Redis (cached highlighted diffs) - Redis (cached highlighted diffs)
We're constantly moving Rugged calls to Gitaly and the progress can be followed through [Gitaly repo](https://gitlab.com/gitlab-org/gitaly).
## Architecture overview ## Architecture overview
### Merge request diffs ### Merge request diffs
...@@ -19,8 +16,9 @@ we fetch the comparison information using `Gitlab::Git::Compare`, which fetches ...@@ -19,8 +16,9 @@ we fetch the comparison information using `Gitlab::Git::Compare`, which fetches
The diffs fetching process _limits_ single file diff sizes and the overall size of the whole diff through a series of constant values. Raw diff files are The diffs fetching process _limits_ single file diff sizes and the overall size of the whole diff through a series of constant values. Raw diff files are
then persisted on `merge_request_diff_files` table. then persisted on `merge_request_diff_files` table.
Even though diffs higher than 10kb are collapsed (`Gitlab::Git::Diff::COLLAPSE_LIMIT`), we still keep them on Postgres. However, diff files over _safety limits_ Even though diffs larger than 10% of the value of `ApplicationSettings#diff_max_patch_bytes` are collapsed,
(see the [Diff limits section](#diff-limits)) are _not_ persisted. we still keep them on Postgres. However, diff files larger than defined _safety limits_
(see the [Diff limits section](#diff-limits)) are _not_ persisted in the database.
In order to present diffs information on the Merge Request diffs page, we: In order to present diffs information on the Merge Request diffs page, we:
...@@ -102,23 +100,20 @@ Gitaly will only return the safe amount of data to be persisted on `merge_reques ...@@ -102,23 +100,20 @@ Gitaly will only return the safe amount of data to be persisted on `merge_reques
Limits that act onto each diff file of a collection. Files number, lines number and files size are considered. Limits that act onto each diff file of a collection. Files number, lines number and files size are considered.
```ruby #### Expandable patches (collapsed)
Gitlab::Git::Diff::COLLAPSE_LIMIT = 10.kilobytes
```
File diff will be collapsed (but be expandable) if it is larger than 10 kilobytes. Diff patches are collapsed when surpassing 10% of the value set in `ApplicationSettings#diff_max_patch_bytes`.
That is, it's equivalent to 10kb if the maximum allowed value is 100kb.
The diff will still be persisted and expandable if the patch size doesn't
surpass `ApplicationSettings#diff_max_patch_bytes`.
*Note:* Although this nomenclature (Collapsing) is also used on Gitaly, this limit is only used on GitLab (hardcoded - not sent to Gitaly). *Note:* Although this nomenclature (Collapsing) is also used on Gitaly, this limit is only used on GitLab (hardcoded - not sent to Gitaly).
Gitaly will only return `Diff.Collapsed` (RPC) when surpassing collection limits. Gitaly will only return `Diff.Collapsed` (RPC) when surpassing collection limits.
```ruby #### Not expandable patches (too large)
Gitlab::Git::Diff::SIZE_LIMIT = 100.kilobytes
```
File diff will not be rendered if it's larger than 100 kilobytes.
*Note:* This limit is currently hardcoded and applied on Gitaly and the RPC returns `Diff.TooLarge` when this limit is surpassed. The patch not be rendered if it's larger than `ApplicationSettings#diff_max_patch_bytes`.
Although we're still also applying it on GitLab, we should remove the redundancy from GitLab once we're confident with the Gitaly integration. Users will see a `This source diff could not be displayed because it is too large` message.
```ruby ```ruby
Commit::DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] = 5000 Commit::DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] = 5000
......
# Diff limits administration
NOTE: **Note:**
Merge requests and branch comparison views will be affected.
CAUTION: **Caution:**
These settings are currently under experimental state. They'll
increase the resource consumption of your instance and should
be edited mindfully.
1. Access **Admin area > Settings > General**
1. Expand **Diff limits**
### Maximum diff patch size
This is the content size each diff file (patch) is allowed to reach before
it's collapsed, without the possibility of being expanded. A link redirecting
to the blob view will be presented for the patches that surpass this limit.
Patches surpassing 10% of this content size will be automatically collapsed,
but expandable (a link to expand the diff will be presented).
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
include Validatable include Validatable
include Attributable include Attributable
ALLOWED_KEYS = %i[junit sast dependency_scanning container_scanning dast].freeze ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast].freeze
attributes ALLOWED_KEYS attributes ALLOWED_KEYS
...@@ -21,6 +21,7 @@ module Gitlab ...@@ -21,6 +21,7 @@ module Gitlab
with_options allow_nil: true do with_options allow_nil: true do
validates :junit, array_of_strings_or_string: true validates :junit, array_of_strings_or_string: true
validates :codequality, array_of_strings_or_string: true
validates :sast, array_of_strings_or_string: true validates :sast, array_of_strings_or_string: true
validates :dependency_scanning, array_of_strings_or_string: true validates :dependency_scanning, array_of_strings_or_string: true
validates :container_scanning, array_of_strings_or_string: true validates :container_scanning, array_of_strings_or_string: true
......
...@@ -49,7 +49,7 @@ variables: ...@@ -49,7 +49,7 @@ variables:
POSTGRES_DB: $CI_ENVIRONMENT_SLUG POSTGRES_DB: $CI_ENVIRONMENT_SLUG
KUBERNETES_VERSION: 1.8.6 KUBERNETES_VERSION: 1.8.6
HELM_VERSION: 2.10.0 HELM_VERSION: 2.11.0
DOCKER_DRIVER: overlay2 DOCKER_DRIVER: overlay2
...@@ -239,7 +239,7 @@ review: ...@@ -239,7 +239,7 @@ review:
- install_dependencies - install_dependencies
- download_chart - download_chart
- ensure_namespace - ensure_namespace
- install_tiller - initialize_tiller
- create_secret - create_secret
- deploy - deploy
- persist_environment_url - persist_environment_url
...@@ -265,6 +265,7 @@ stop_review: ...@@ -265,6 +265,7 @@ stop_review:
GIT_STRATEGY: none GIT_STRATEGY: none
script: script:
- install_dependencies - install_dependencies
- initialize_tiller
- delete - delete
environment: environment:
name: review/$CI_COMMIT_REF_NAME name: review/$CI_COMMIT_REF_NAME
...@@ -299,7 +300,7 @@ staging: ...@@ -299,7 +300,7 @@ staging:
- install_dependencies - install_dependencies
- download_chart - download_chart
- ensure_namespace - ensure_namespace
- install_tiller - initialize_tiller
- create_secret - create_secret
- deploy - deploy
environment: environment:
...@@ -323,7 +324,7 @@ canary: ...@@ -323,7 +324,7 @@ canary:
- install_dependencies - install_dependencies
- download_chart - download_chart
- ensure_namespace - ensure_namespace
- install_tiller - initialize_tiller
- create_secret - create_secret
- deploy canary - deploy canary
environment: environment:
...@@ -344,7 +345,7 @@ canary: ...@@ -344,7 +345,7 @@ canary:
- install_dependencies - install_dependencies
- download_chart - download_chart
- ensure_namespace - ensure_namespace
- install_tiller - initialize_tiller
- create_secret - create_secret
- deploy - deploy
- delete canary - delete canary
...@@ -392,7 +393,7 @@ production_manual: ...@@ -392,7 +393,7 @@ production_manual:
- install_dependencies - install_dependencies
- download_chart - download_chart
- ensure_namespace - ensure_namespace
- install_tiller - initialize_tiller
- create_secret - create_secret
- deploy rollout $ROLLOUT_PERCENTAGE - deploy rollout $ROLLOUT_PERCENTAGE
- scale stable $((100-ROLLOUT_PERCENTAGE)) - scale stable $((100-ROLLOUT_PERCENTAGE))
...@@ -651,7 +652,12 @@ rollout 100%: ...@@ -651,7 +652,12 @@ rollout 100%:
curl "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx curl "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx
mv linux-amd64/helm /usr/bin/ mv linux-amd64/helm /usr/bin/
mv linux-amd64/tiller /usr/bin/
helm version --client helm version --client
tiller -version
helm init --client-only
helm plugin install https://github.com/adamreese/helm-local
curl -L -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl" 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
...@@ -758,10 +764,13 @@ rollout 100%: ...@@ -758,10 +764,13 @@ rollout 100%:
echo "" echo ""
} }
function install_tiller() { function initialize_tiller() {
echo "Checking Tiller..." echo "Checking Tiller..."
helm init --upgrade
kubectl rollout status -n "$TILLER_NAMESPACE" -w "deployment/tiller-deploy" helm local start
helm local status
export HELM_HOST=":44134"
if ! helm version --debug; then if ! helm version --debug; then
echo "Failed to init Tiller." echo "Failed to init Tiller."
return 1 return 1
......
...@@ -19,13 +19,17 @@ module Gitlab ...@@ -19,13 +19,17 @@ module Gitlab
alias_method :expanded?, :expanded alias_method :expanded?, :expanded
SERIALIZE_KEYS = %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large).freeze # The default maximum content size to display a diff patch.
#
# If this value ever changes, make sure to create a migration to update
# current records, and default of `ApplicationSettings#diff_max_patch_bytes`.
DEFAULT_MAX_PATCH_BYTES = 100.kilobytes
# The maximum size of a diff to display. # This is a limitation applied on the source (Gitaly), therefore we don't allow
SIZE_LIMIT = 100.kilobytes # persisting limits over that.
MAX_PATCH_BYTES_UPPER_BOUND = 500.kilobytes
# The maximum size before a diff is collapsed. SERIALIZE_KEYS = %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large).freeze
COLLAPSE_LIMIT = 10.kilobytes
class << self class << self
def between(repo, head, base, options = {}, *paths) def between(repo, head, base, options = {}, *paths)
...@@ -105,6 +109,26 @@ module Gitlab ...@@ -105,6 +109,26 @@ module Gitlab
def binary_message(old_path, new_path) def binary_message(old_path, new_path)
"Binary files #{old_path} and #{new_path} differ\n" "Binary files #{old_path} and #{new_path} differ\n"
end end
# Returns the limit of bytes a single diff file can reach before it
# appears as 'collapsed' for end-users.
# By convention, it's 10% of the persisted `diff_max_patch_bytes`.
#
# Example: If we have 100k for the `diff_max_patch_bytes`, it will be 10k by
# default.
#
# Patches surpassing this limit should still be persisted in the database.
def patch_safe_limit_bytes
patch_hard_limit_bytes / 10
end
# Returns the limit for a single diff file (patch).
#
# Patches surpassing this limit shouldn't be persisted in the database
# and will be presented as 'too large' for end-users.
def patch_hard_limit_bytes
Gitlab::CurrentSettings.diff_max_patch_bytes
end
end end
def initialize(raw_diff, expanded: true) def initialize(raw_diff, expanded: true)
...@@ -150,7 +174,7 @@ module Gitlab ...@@ -150,7 +174,7 @@ module Gitlab
def too_large? def too_large?
if @too_large.nil? if @too_large.nil?
@too_large = @diff.bytesize >= SIZE_LIMIT @too_large = @diff.bytesize >= self.class.patch_hard_limit_bytes
else else
@too_large @too_large
end end
...@@ -168,7 +192,7 @@ module Gitlab ...@@ -168,7 +192,7 @@ module Gitlab
def collapsed? def collapsed?
return @collapsed if defined?(@collapsed) return @collapsed if defined?(@collapsed)
@collapsed = !expanded && @diff.bytesize >= COLLAPSE_LIMIT @collapsed = !expanded && @diff.bytesize >= self.class.patch_safe_limit_bytes
end end
def collapse! def collapse!
...@@ -219,30 +243,6 @@ module Gitlab ...@@ -219,30 +243,6 @@ module Gitlab
collapse! collapse!
end end
end end
# If the patch surpasses any of the diff limits it calls the appropiate
# prune method and returns true. Otherwise returns false.
def prune_large_patch(patch)
size = 0
patch.each_hunk do |hunk|
hunk.each_line do |line|
size += line.content.bytesize
if size >= SIZE_LIMIT
too_large!
return true # rubocop:disable Cop/AvoidReturnFromBlocks
end
end
end
if !expanded && size >= COLLAPSE_LIMIT
collapse!
return true
end
false
end
end end
end end
end end
...@@ -19,7 +19,7 @@ module Gitlab ...@@ -19,7 +19,7 @@ module Gitlab
limits[:safe_max_files] = [limits[:max_files], DEFAULT_LIMITS[:max_files]].min limits[:safe_max_files] = [limits[:max_files], DEFAULT_LIMITS[:max_files]].min
limits[:safe_max_lines] = [limits[:max_lines], DEFAULT_LIMITS[:max_lines]].min limits[:safe_max_lines] = [limits[:max_lines], DEFAULT_LIMITS[:max_lines]].min
limits[:safe_max_bytes] = limits[:safe_max_files] * 5.kilobytes # Average 5 KB per file limits[:safe_max_bytes] = limits[:safe_max_files] * 5.kilobytes # Average 5 KB per file
limits[:max_patch_bytes] = Gitlab::Git::Diff::SIZE_LIMIT limits[:max_patch_bytes] = Gitlab::Git::Diff.patch_hard_limit_bytes
OpenStruct.new(limits) OpenStruct.new(limits)
end end
......
...@@ -2640,7 +2640,14 @@ msgstr "" ...@@ -2640,7 +2640,14 @@ msgstr ""
msgid "Details" msgid "Details"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Detect host keys" msgid "Detect host keys"
=======
msgid "Diff content limits"
msgstr ""
msgid "Diff limits"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "Diffs|No file name available" msgid "Diffs|No file name available"
...@@ -4595,10 +4602,38 @@ msgstr "" ...@@ -4595,10 +4602,38 @@ msgstr ""
msgid "Markdown enabled" msgid "Markdown enabled"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Maven Metadata" msgid "Maven Metadata"
msgstr "" msgstr ""
msgid "Maven package" msgid "Maven package"
=======
msgid "MarkdownToolbar|Add a bullet list"
msgstr ""
msgid "MarkdownToolbar|Add a link"
msgstr ""
msgid "MarkdownToolbar|Add a numbered list"
msgstr ""
msgid "MarkdownToolbar|Add a task list"
msgstr ""
msgid "MarkdownToolbar|Add bold text"
msgstr ""
msgid "MarkdownToolbar|Add italic text"
msgstr ""
msgid "MarkdownToolbar|Go full screen"
msgstr ""
msgid "MarkdownToolbar|Insert a quote"
msgstr ""
msgid "MarkdownToolbar|Insert code"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "Max access level" msgid "Max access level"
......
...@@ -6,12 +6,7 @@ module QA ...@@ -6,12 +6,7 @@ module QA
module GroupsFilter module GroupsFilter
def self.included(base) def self.included(base)
base.view 'app/views/shared/groups/_search_form.html.haml' do base.view 'app/views/shared/groups/_search_form.html.haml' do
element :groups_filter, 'search_field_tag :filter' element :groups_filter
element :groups_filter_placeholder, 'Search by name'
end
base.view 'app/views/shared/groups/_empty_state.html.haml' do
element :groups_empty_state
end end
base.view 'app/assets/javascripts/groups/components/groups.vue' do base.view 'app/assets/javascripts/groups/components/groups.vue' do
...@@ -21,13 +16,22 @@ module QA ...@@ -21,13 +16,22 @@ module QA
private private
def filter_by_name(name) def has_filtered_group?(name)
# Filter and submit to reload the page and only retrieve the filtered results
find_element(:groups_filter).set(name).send_keys(:return)
# Since we submitted after filtering, the presence of
# groups_list_tree_container means we have the complete filtered list
# of groups
wait(reload: false) do wait(reload: false) do
page.has_css?(element_selector_css(:groups_empty_state)) ||
page.has_css?(element_selector_css(:groups_list_tree_container)) page.has_css?(element_selector_css(:groups_list_tree_container))
end end
fill_in 'Search by name', with: name # If there are no groups we'll know immediately because we filtered the list
return false if page.has_text?('No groups or projects matched your search', wait: 0)
# The name will be present as filter input so we check for a link, not text
page.has_link?(name, wait: 0)
end end
end end
end end
......
# frozen_string_literal: true
module QA module QA
module Page module Page
module Dashboard module Dashboard
...@@ -14,9 +16,7 @@ module QA ...@@ -14,9 +16,7 @@ module QA
end end
def has_group?(name) def has_group?(name)
filter_by_name(name) has_filtered_group?(name)
page.has_link?(name)
end end
def go_to_group(name) def go_to_group(name)
......
# frozen_string_literal: true
module QA module QA
module Page module Page
module Group module Group
...@@ -25,11 +27,7 @@ module QA ...@@ -25,11 +27,7 @@ module QA
end end
def has_subgroup?(name) def has_subgroup?(name)
filter_by_name(name) has_filtered_group?(name)
page.has_text?(/#{name}|No groups or projects matched your search/, wait: 60)
page.has_text?(name, wait: 0)
end end
def go_to_new_subgroup def go_to_new_subgroup
......
...@@ -9,6 +9,7 @@ module QA ...@@ -9,6 +9,7 @@ module QA
element :admin_sidebar_submenu element :admin_sidebar_submenu
element :admin_settings_item element :admin_settings_item
element :admin_settings_repository_item element :admin_settings_repository_item
<<<<<<< HEAD
end end
def go_to_repository_settings def go_to_repository_settings
...@@ -36,6 +37,35 @@ module QA ...@@ -36,6 +37,35 @@ module QA
end end
end end
=======
end
def go_to_repository_settings
hover_settings do
within_submenu do
click_element :admin_settings_repository_item
end
end
end
private
def hover_settings
within_sidebar do
scroll_to_element(:admin_settings_item)
find_element(:admin_settings_item).hover
yield
end
end
def within_sidebar
within_element(:admin_sidebar) do
yield
end
end
>>>>>>> upstream/master
def within_submenu def within_submenu
within_element(:admin_sidebar_submenu) do within_element(:admin_sidebar_submenu) do
yield yield
......
...@@ -19,10 +19,42 @@ describe Projects::ArtifactsController do ...@@ -19,10 +19,42 @@ describe Projects::ArtifactsController do
end end
describe 'GET download' do describe 'GET download' do
subject { get :download, namespace_id: project.namespace, project_id: project, job_id: job, file_type: file_type }
context 'when no file type is supplied' do
let(:file_type) { nil }
it 'sends the artifacts file' do it 'sends the artifacts file' do
expect(controller).to receive(:send_file).with(job.artifacts_file.path, hash_including(disposition: 'attachment')).and_call_original expect(controller).to receive(:send_file).with(job.artifacts_file.path, hash_including(disposition: 'attachment')).and_call_original
get :download, namespace_id: project.namespace, project_id: project, job_id: job subject
end
end
context 'when a file type is supplied' do
context 'when an invalid file type is supplied' do
let(:file_type) { 'invalid' }
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
context 'when codequality file type is supplied' do
let(:file_type) { 'codequality' }
before do
create(:ci_job_artifact, :codequality, job: job)
end
it 'sends the codequality report' do
expect(controller).to receive(:send_file).with(job.job_artifacts_codequality.file.path, hash_including(disposition: 'attachment')).and_call_original
subject
end
end
end end
end end
......
...@@ -117,6 +117,16 @@ FactoryBot.define do ...@@ -117,6 +117,16 @@ FactoryBot.define do
end end
end end
trait :codequality do
file_type :codequality
file_format :gzip
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/codequality/codequality.json.gz'), 'application/x-gzip')
end
end
trait :correct_checksum do trait :correct_checksum do
after(:build) do |artifact, evaluator| after(:build) do |artifact, evaluator|
artifact.file_sha256 = Digest::SHA256.file(artifact.file.path).hexdigest artifact.file_sha256 = Digest::SHA256.file(artifact.file.path).hexdigest
......
...@@ -13,6 +13,10 @@ FactoryBot.define do ...@@ -13,6 +13,10 @@ FactoryBot.define do
state :opened state :opened
end end
trait :locked do
discussion_locked true
end
trait :closed do trait :closed do
state :closed state :closed
closed_at { Time.now } closed_at { Time.now }
......
...@@ -56,6 +56,24 @@ describe 'Issuables Close/Reopen/Report toggle' do ...@@ -56,6 +56,24 @@ describe 'Issuables Close/Reopen/Report toggle' do
end end
it_behaves_like 'an issuable close/reopen/report toggle' it_behaves_like 'an issuable close/reopen/report toggle'
context 'when the issue is closed and locked' do
let(:issuable) { create(:issue, :closed, :locked, project: project) }
it 'hides the reopen button' do
expect(page).not_to have_link('Reopen issue')
end
context 'when the issue author is the current user' do
before do
issuable.update(author: user)
end
it 'hides the reopen button' do
expect(page).not_to have_link('Reopen issue')
end
end
end
end end
context 'when user doesnt have permission to update' do context 'when user doesnt have permission to update' do
...@@ -93,6 +111,28 @@ describe 'Issuables Close/Reopen/Report toggle' do ...@@ -93,6 +111,28 @@ describe 'Issuables Close/Reopen/Report toggle' do
end end
it_behaves_like 'an issuable close/reopen/report toggle' it_behaves_like 'an issuable close/reopen/report toggle'
context 'when the merge request is merged' do
let(:issuable) { create(:merge_request, :merged, source_project: project) }
it 'shows only the `Report abuse` and `Edit` button' do
expect(page).to have_link('Report abuse')
expect(page).to have_link('Edit')
expect(page).not_to have_link('Close merge request')
expect(page).not_to have_link('Reopen merge request')
end
context 'when the merge request author is the current user' do
let(:issuable) { create(:merge_request, :merged, source_project: project, author: user) }
it 'shows only the `Edit` button' do
expect(page).to have_link('Edit')
expect(page).not_to have_link('Report abuse')
expect(page).not_to have_link('Close merge request')
expect(page).not_to have_link('Reopen merge request')
end
end
end
end end
context 'when user doesnt have permission to update' do context 'when user doesnt have permission to update' do
......
...@@ -3,6 +3,12 @@ require 'spec_helper' ...@@ -3,6 +3,12 @@ require 'spec_helper'
describe 'Create notes on issues', :js do describe 'Create notes on issues', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
def submit_comment(text)
fill_in 'note[note]', with: text
click_button 'Comment'
wait_for_requests
end
shared_examples 'notes with reference' do shared_examples 'notes with reference' do
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:note_text) { "Check #{mention.to_reference}" } let(:note_text) { "Check #{mention.to_reference}" }
...@@ -12,10 +18,7 @@ describe 'Create notes on issues', :js do ...@@ -12,10 +18,7 @@ describe 'Create notes on issues', :js do
sign_in(user) sign_in(user)
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
fill_in 'note[note]', with: note_text submit_comment(note_text)
click_button 'Comment'
wait_for_requests
end end
it 'creates a note with reference and cross references the issue' do it 'creates a note with reference and cross references the issue' do
...@@ -74,4 +77,16 @@ describe 'Create notes on issues', :js do ...@@ -74,4 +77,16 @@ describe 'Create notes on issues', :js do
let(:mention) { create(:merge_request, source_project: project) } let(:mention) { create(:merge_request, source_project: project) }
end end
end end
it 'highlights the current user in a comment' do
project = create(:project)
issue = create(:issue, project: project)
project.add_developer(user)
sign_in(user)
visit project_issue_path(project, issue)
submit_comment("@#{user.username} note to self")
expect(page).to have_selector '.gfm-project_member.current-user', text: user.username
end
end end
This source diff could not be displayed because it is too large. You can view the blob instead.
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
describe('highlightCurrentUser', () => {
let rootElement;
let elements;
beforeEach(() => {
setFixtures(`
<div id="dummy-root-element">
<div data-user="1">@first</div>
<div data-user="2">@second</div>
</div>
`);
rootElement = document.getElementById('dummy-root-element');
elements = rootElement.querySelectorAll('[data-user]');
});
describe('without current user', () => {
beforeEach(() => {
window.gon = window.gon || {};
window.gon.current_user_id = null;
});
afterEach(() => {
delete window.gon.current_user_id;
});
it('does not highlight the user', () => {
const initialHtml = rootElement.outerHTML;
highlightCurrentUser(elements);
expect(rootElement.outerHTML).toBe(initialHtml);
});
});
describe('with current user', () => {
beforeEach(() => {
window.gon = window.gon || {};
window.gon.current_user_id = 2;
});
afterEach(() => {
delete window.gon.current_user_id;
});
it('highlights current user', () => {
highlightCurrentUser(elements);
expect(elements.length).toBe(2);
expect(elements[0]).not.toHaveClass('current-user');
expect(elements[1]).toHaveClass('current-user');
});
});
});
...@@ -21,7 +21,7 @@ describe('init markdown', () => { ...@@ -21,7 +21,7 @@ describe('init markdown', () => {
textArea.selectionStart = 0; textArea.selectionStart = 0;
textArea.selectionEnd = 0; textArea.selectionEnd = 0;
insertMarkdownText(textArea, textArea.value, '*', null, '', false); insertMarkdownText({ textArea, text: textArea.value, tag: '*', blockTag: null, selected: '', wrap: false });
expect(textArea.value).toEqual(`${initialValue}* `); expect(textArea.value).toEqual(`${initialValue}* `);
}); });
...@@ -32,7 +32,7 @@ describe('init markdown', () => { ...@@ -32,7 +32,7 @@ describe('init markdown', () => {
textArea.value = initialValue; textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length); textArea.setSelectionRange(initialValue.length, initialValue.length);
insertMarkdownText(textArea, textArea.value, '*', null, '', false); insertMarkdownText({ textArea, text: textArea.value, tag: '*', blockTag: null, selected: '', wrap: false });
expect(textArea.value).toEqual(`${initialValue}\n* `); expect(textArea.value).toEqual(`${initialValue}\n* `);
}); });
...@@ -43,7 +43,7 @@ describe('init markdown', () => { ...@@ -43,7 +43,7 @@ describe('init markdown', () => {
textArea.value = initialValue; textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length); textArea.setSelectionRange(initialValue.length, initialValue.length);
insertMarkdownText(textArea, textArea.value, '*', null, '', false); insertMarkdownText({ textArea, text: textArea.value, tag: '*', blockTag: null, selected: '', wrap: false });
expect(textArea.value).toEqual(`${initialValue}* `); expect(textArea.value).toEqual(`${initialValue}* `);
}); });
...@@ -54,9 +54,70 @@ describe('init markdown', () => { ...@@ -54,9 +54,70 @@ describe('init markdown', () => {
textArea.value = initialValue; textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length); textArea.setSelectionRange(initialValue.length, initialValue.length);
insertMarkdownText(textArea, textArea.value, '*', null, '', false); insertMarkdownText({ textArea, text: textArea.value, tag: '*', blockTag: null, selected: '', wrap: false });
expect(textArea.value).toEqual(`${initialValue}* `); expect(textArea.value).toEqual(`${initialValue}* `);
}); });
}); });
describe('with selection', () => {
const text = 'initial selected value';
const selected = 'selected';
beforeEach(() => {
textArea.value = text;
const selectedIndex = text.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
});
it('applies the tag to the selected value', () => {
insertMarkdownText({ textArea, text: textArea.value, tag: '*', blockTag: null, selected, wrap: true });
expect(textArea.value).toEqual(text.replace(selected, `*${selected}*`));
});
it('replaces the placeholder in the tag', () => {
insertMarkdownText({ textArea, text: textArea.value, tag: '[{text}](url)', blockTag: null, selected, wrap: false });
expect(textArea.value).toEqual(text.replace(selected, `[${selected}](url)`));
});
describe('and text to be selected', () => {
const tag = '[{text}](url)';
const select = 'url';
it('selects the text', () => {
insertMarkdownText({ textArea,
text: textArea.value,
tag,
blockTag: null,
selected,
wrap: false,
select });
const expectedText = text.replace(selected, `[${selected}](url)`);
expect(textArea.value).toEqual(expectedText);
expect(textArea.selectionStart).toEqual(expectedText.indexOf(select));
expect(textArea.selectionEnd).toEqual(expectedText.indexOf(select) + select.length);
});
it('selects the right text when multiple tags are present', () => {
const initialValue = `${tag} ${tag} ${selected}`;
textArea.value = initialValue;
const selectedIndex = initialValue.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
insertMarkdownText({ textArea,
text: textArea.value,
tag,
blockTag: null,
selected,
wrap: false,
select });
const expectedText = initialValue.replace(selected, `[${selected}](url)`);
expect(textArea.value).toEqual(expectedText);
expect(textArea.selectionStart).toEqual(expectedText.lastIndexOf(select));
expect(textArea.selectionEnd).toEqual(expectedText.lastIndexOf(select) + select.length);
});
});
});
}); });
...@@ -153,7 +153,7 @@ describe('Markdown field component', () => { ...@@ -153,7 +153,7 @@ describe('Markdown field component', () => {
const textarea = vm.$el.querySelector('textarea'); const textarea = vm.$el.querySelector('textarea');
textarea.setSelectionRange(0, 0); textarea.setSelectionRange(0, 0);
vm.$el.querySelectorAll('.js-md')[4].click(); vm.$el.querySelectorAll('.js-md')[5].click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
...@@ -168,7 +168,7 @@ describe('Markdown field component', () => { ...@@ -168,7 +168,7 @@ describe('Markdown field component', () => {
const textarea = vm.$el.querySelector('textarea'); const textarea = vm.$el.querySelector('textarea');
textarea.setSelectionRange(0, 50); textarea.setSelectionRange(0, 50);
vm.$el.querySelectorAll('.js-md')[4].click(); vm.$el.querySelectorAll('.js-md')[5].click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
......
...@@ -18,7 +18,7 @@ describe('Markdown field header component', () => { ...@@ -18,7 +18,7 @@ describe('Markdown field header component', () => {
}); });
it('renders markdown buttons', () => { it('renders markdown buttons', () => {
expect(vm.$el.querySelectorAll('.js-md').length).toBe(7); expect(vm.$el.querySelectorAll('.js-md').length).toBe(8);
}); });
it('renders `write` link as active when previewMarkdown is false', () => { it('renders `write` link as active when previewMarkdown is false', () => {
......
...@@ -33,6 +33,7 @@ describe Gitlab::Ci::Config::Entry::Reports do ...@@ -33,6 +33,7 @@ describe Gitlab::Ci::Config::Entry::Reports do
where(:keyword, :file) do where(:keyword, :file) do
:junit | 'junit.xml' :junit | 'junit.xml'
:codequality | 'codequality.json'
:sast | 'gl-sast-report.json' :sast | 'gl-sast-report.json'
:dependency_scanning | 'gl-dependency-scanning-report.json' :dependency_scanning | 'gl-dependency-scanning-report.json'
:container_scanning | 'gl-container-scanning-report.json' :container_scanning | 'gl-container-scanning-report.json'
......
...@@ -592,4 +592,19 @@ describe ApplicationSetting do ...@@ -592,4 +592,19 @@ describe ApplicationSetting do
it { is_expected.to eq(result) } it { is_expected.to eq(result) }
end end
end end
context 'diff limit settings' do
describe '#diff_max_patch_bytes' do
context 'validations' do
it { is_expected.to validate_presence_of(:diff_max_patch_bytes) }
it do
is_expected.to validate_numericality_of(:diff_max_patch_bytes)
.only_integer
.is_greater_than_or_equal_to(Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES)
.is_less_than_or_equal_to(Gitlab::Git::Diff::MAX_PATCH_BYTES_UPPER_BOUND)
end
end
end
end
end end
...@@ -1279,6 +1279,19 @@ describe Ci::Build do ...@@ -1279,6 +1279,19 @@ describe Ci::Build do
end end
end end
describe '#artifacts_file_for_type' do
let(:build) { create(:ci_build, :artifacts) }
let(:file_type) { :archive }
subject { build.artifacts_file_for_type(file_type) }
it 'queries artifacts for type' do
expect(build).to receive_message_chain(:job_artifacts, :find_by).with(file_type: Ci::JobArtifact.file_types[file_type])
subject
end
end
describe '#merge_request' do describe '#merge_request' do
def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
create(factory, source_project: pipeline.project, create(factory, source_project: pipeline.project,
......
...@@ -34,7 +34,7 @@ describe Ci::JobArtifact do ...@@ -34,7 +34,7 @@ describe Ci::JobArtifact do
describe '.erasable' do describe '.erasable' do
subject { described_class.erasable } subject { described_class.erasable }
context 'when there is am erasable artifact' do context 'when there is an erasable artifact' do
let!(:artifact) { create(:ci_job_artifact, :junit) } let!(:artifact) { create(:ci_job_artifact, :junit) }
it { is_expected.to eq([artifact]) } it { is_expected.to eq([artifact]) }
......
...@@ -82,6 +82,27 @@ describe Namespace do ...@@ -82,6 +82,27 @@ describe Namespace do
it { expect(namespace.human_name).to eq(namespace.owner_name) } it { expect(namespace.human_name).to eq(namespace.owner_name) }
end end
describe '#first_project_with_container_registry_tags' do
let(:container_repository) { create(:container_repository) }
let!(:project) { create(:project, namespace: namespace, container_repositories: [container_repository]) }
before do
stub_container_registry_config(enabled: true)
end
it 'returns the project' do
stub_container_registry_tags(repository: :any, tags: ['tag'])
expect(namespace.first_project_with_container_registry_tags).to eq(project)
end
it 'returns no project' do
stub_container_registry_tags(repository: :any, tags: nil)
expect(namespace.first_project_with_container_registry_tags).to be_nil
end
end
describe '.search' do describe '.search' do
let(:namespace) { create(:namespace) } let(:namespace) { create(:namespace) }
...@@ -184,7 +205,8 @@ describe Namespace do ...@@ -184,7 +205,8 @@ describe Namespace do
end end
it 'raises an error about not movable project' do it 'raises an error about not movable project' do
expect { namespace.move_dir }.to raise_error(/Namespace cannot be moved/) expect { namespace.move_dir }.to raise_error(Gitlab::UpdatePathError,
/Namespace .* cannot be moved/)
end end
end end
end end
......
...@@ -4310,6 +4310,49 @@ describe Project do ...@@ -4310,6 +4310,49 @@ describe Project do
end end
end end
context '#members_among' do
let(:users) { create_list(:user, 3) }
set(:group) { create(:group) }
set(:project) { create(:project, namespace: group) }
before do
project.add_guest(users.first)
project.group.add_maintainer(users.last)
end
context 'when users is an Array' do
it 'returns project members among the users' do
expect(project.members_among(users)).to eq([users.first, users.last])
end
it 'maintains input order' do
expect(project.members_among(users.reverse)).to eq([users.last, users.first])
end
it 'returns empty array if users is empty' do
result = project.members_among([])
expect(result).to be_empty
end
end
context 'when users is a relation' do
it 'returns project members among the users' do
result = project.members_among(User.where(id: users.map(&:id)))
expect(result).to be_a(ActiveRecord::Relation)
expect(result).to eq([users.first, users.last])
end
it 'returns empty relation if users is empty' do
result = project.members_among(User.none)
expect(result).to be_a(ActiveRecord::Relation)
expect(result).to be_empty
end
end
end
def rugged_config def rugged_config
Gitlab::GitalyClient::StorageSettings.allow_disk_access do Gitlab::GitalyClient::StorageSettings.allow_disk_access do
project.repository.rugged.config project.repository.rugged.config
......
...@@ -112,7 +112,7 @@ describe IssuePolicy do ...@@ -112,7 +112,7 @@ describe IssuePolicy do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) } let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
let(:issue_no_assignee) { create(:issue, project: project) } let(:issue_no_assignee) { create(:issue, project: project) }
let(:issue_locked) { create(:issue, project: project, discussion_locked: true, author: author, assignees: [assignee]) } let(:issue_locked) { create(:issue, :locked, project: project, author: author, assignees: [assignee]) }
before do before do
project.add_guest(guest) project.add_guest(guest)
......
...@@ -68,7 +68,8 @@ describe API::Settings, 'Settings' do ...@@ -68,7 +68,8 @@ describe API::Settings, 'Settings' do
enforce_terms: true, enforce_terms: true,
terms: 'Hello world!', terms: 'Hello world!',
performance_bar_allowed_group_path: group.full_path, performance_bar_allowed_group_path: group.full_path,
instance_statistics_visibility_private: true instance_statistics_visibility_private: true,
diff_max_patch_bytes: 150_000
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['default_projects_limit']).to eq(3) expect(json_response['default_projects_limit']).to eq(3)
...@@ -94,6 +95,7 @@ describe API::Settings, 'Settings' do ...@@ -94,6 +95,7 @@ describe API::Settings, 'Settings' do
expect(json_response['terms']).to eq('Hello world!') expect(json_response['terms']).to eq('Hello world!')
expect(json_response['performance_bar_allowed_group_id']).to eq(group.id) expect(json_response['performance_bar_allowed_group_id']).to eq(group.id)
expect(json_response['instance_statistics_visibility_private']).to be(true) expect(json_response['instance_statistics_visibility_private']).to be(true)
expect(json_response['diff_max_patch_bytes']).to eq(150_000)
end end
end end
......
...@@ -26,7 +26,8 @@ describe Ci::RetryBuildService do ...@@ -26,7 +26,8 @@ describe Ci::RetryBuildService do
erased_at auto_canceled_by job_artifacts job_artifacts_archive erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_dependency_scanning job_artifacts_sast job_artifacts_dependency_scanning
job_artifacts_container_scanning job_artifacts_dast].freeze job_artifacts_container_scanning job_artifacts_dast
job_artifacts_codequality].freeze
IGNORE_ACCESSORS = IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections %i[type lock_version target_url base_tags trace_sections
......
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