Commit bb516340 authored by GitLab Bot's avatar GitLab Bot

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

# Conflicts:
#	app/services/members/approve_access_request_service.rb
#	app/services/members/create_service.rb
#	app/services/members/destroy_service.rb
#	app/services/members/update_service.rb
#	app/views/projects/merge_requests/show.html.haml
#	config/webpack.config.js

[ci skip]
parents c2501d08 0be4a77d
...@@ -11,8 +11,8 @@ engines: ...@@ -11,8 +11,8 @@ engines:
exclude_paths: exclude_paths:
- "lib/api/v3/*" - "lib/api/v3/*"
eslint: eslint:
# eslint-plugin-vue is locked to version 2 in codeclimate, we need version 4 enabled: true
enabled: false channel: "eslint-4"
rubocop: rubocop:
enabled: true enabled: true
channel: "gitlab-rubocop-0-52-1" channel: "gitlab-rubocop-0-52-1"
......
...@@ -242,10 +242,16 @@ export default class LabelsSelect { ...@@ -242,10 +242,16 @@ export default class LabelsSelect {
filterable: true, filterable: true,
selected: $dropdown.data('selected') || [], selected: $dropdown.data('selected') || [],
toggleLabel: function(selected, el) { toggleLabel: function(selected, el) {
var $dropdownParent = $dropdown.parent();
var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
var isSelected = el !== null ? el.hasClass('is-active') : false; var isSelected = el !== null ? el.hasClass('is-active') : false;
var title = selected.title; var title = selected.title;
var selectedLabels = this.selected; var selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
$dropdownParent.find('.dropdown-input-clear').trigger('click');
}
if (selected.id === 0) { if (selected.id === 0) {
this.selected = []; this.selected = [];
return 'No Label'; return 'No Label';
......
import '~/profile/gl_crop';
import Profile from '~/profile/profile';
document.addEventListener('DOMContentLoaded', () => {
$(document).on('input.ssh_key', '#key_key', function () { // eslint-disable-line func-names
const $title = $('#key_title');
const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
// Extract the SSH Key title from its comment
if (comment && comment.length > 1) {
$title.val(comment[1]).change();
}
});
new Profile(); // eslint-disable-line no-new
});
import initIssuableSidebar from '~/init_issuable_sidebar'; import initIssuableSidebar from '~/init_issuable_sidebar';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import Issue from '~/issue'; import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable'; import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
...@@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => {
new ShortcutsIssuable(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar(); initIssuableSidebar();
initSidebarBundle();
}); });
import initSidebarBundle from '~/sidebar/sidebar_bundle';
document.addEventListener('DOMContentLoaded', initSidebarBundle);
import initSidebarBundle from '~/sidebar/sidebar_bundle';
document.addEventListener('DOMContentLoaded', initSidebarBundle);
...@@ -3,6 +3,7 @@ import ZenMode from '~/zen_mode'; ...@@ -3,6 +3,7 @@ import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes'; import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar'; import initIssuableSidebar from '~/init_issuable_sidebar';
import initDiffNotes from '~/diff_notes/diff_notes_bundle'; import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import ShortcutsIssuable from '~/shortcuts_issuable'; import ShortcutsIssuable from '~/shortcuts_issuable';
import Diff from '~/diff'; import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils'; import { handleLocationHash } from '~/lib/utils/common_utils';
...@@ -15,6 +16,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -15,6 +16,7 @@ document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar(); initIssuableSidebar();
initSidebarBundle();
initNotes(); initNotes();
initDiffNotes(); initDiffNotes();
initPipelines(); initPipelines();
......
import initSnippet from '~/snippet/snippet_bundle';
import initForm from '~/pages/projects/init_form'; import initForm from '~/pages/projects/init_form';
document.addEventListener('DOMContentLoaded', () => initForm($('.snippet-form'))); document.addEventListener('DOMContentLoaded', () => {
initSnippet();
initForm($('.snippet-form'));
});
import initSnippet from '~/snippet/snippet_bundle';
import initForm from '~/pages/projects/init_form'; import initForm from '~/pages/projects/init_form';
document.addEventListener('DOMContentLoaded', () => initForm($('.snippet-form'))); document.addEventListener('DOMContentLoaded', () => {
initSnippet();
initForm($('.snippet-form'));
});
import initSnippet from '~/snippet/snippet_bundle';
import form from '../form'; import form from '../form';
document.addEventListener('DOMContentLoaded', form); document.addEventListener('DOMContentLoaded', () => {
initSnippet();
form();
});
import initSnippet from '~/snippet/snippet_bundle';
import form from '../form'; import form from '../form';
document.addEventListener('DOMContentLoaded', form); document.addEventListener('DOMContentLoaded', () => {
initSnippet();
form();
});
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ /* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { getPagePath } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import flash from '../flash'; import flash from '../flash';
((global) => { export default class Profile {
class Profile {
constructor({ form } = {}) { constructor({ form } = {}) {
this.onSubmitForm = this.onSubmitForm.bind(this); this.onSubmitForm = this.onSubmitForm.bind(this);
this.form = form || $('.edit-user'); this.form = form || $('.edit-user');
...@@ -84,20 +82,4 @@ import flash from '../flash'; ...@@ -84,20 +82,4 @@ import flash from '../flash';
multiEditRadios.filter('[value=off]').prop('checked', true); multiEditRadios.filter('[value=off]').prop('checked', true);
} }
} }
} }
$(function() {
$(document).on('input.ssh_key', '#key_key', function() {
const $title = $('#key_title');
const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
// Extract the SSH Key title from its comment
if (comment && comment.length > 1) {
return $title.val(comment[1]).change();
}
});
if (getPagePath() === 'profiles') {
return new Profile();
}
});
})(window.gl || (window.gl = {}));
import './gl_crop';
import './profile';
import Mediator from './sidebar_mediator'; import Mediator from './sidebar_mediator';
import { mountSidebar, getSidebarOptions } from './mount_sidebar'; import { mountSidebar, getSidebarOptions } from './mount_sidebar';
function domContentLoaded() { export default () => {
const mediator = new Mediator(getSidebarOptions()); const mediator = new Mediator(getSidebarOptions());
mediator.fetch(); mediator.fetch();
mountSidebar(mediator); mountSidebar(mediator);
} };
document.addEventListener('DOMContentLoaded', domContentLoaded);
export default domContentLoaded;
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */ /* global ace */
(function() { export default () => {
$(function() { const editor = ace.edit('editor');
var editor = ace.edit("editor");
$(".snippet-form-holder form").on('submit', function() { $('.snippet-form-holder form').on('submit', () => {
$(".snippet-file-content").val(editor.getValue()); $('.snippet-file-content').val(editor.getValue());
}); });
}); };
}).call(window);
...@@ -449,7 +449,7 @@ class User < ActiveRecord::Base ...@@ -449,7 +449,7 @@ class User < ActiveRecord::Base
end end
def self.non_internal def self.non_internal
where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)]) where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND "))
end end
# #
......
module Members module Members
class ApproveAccessRequestService < Members::BaseService class ApproveAccessRequestService < Members::BaseService
<<<<<<< HEAD
prepend EE::Members::ApproveAccessRequestService prepend EE::Members::ApproveAccessRequestService
=======
>>>>>>> upstream/master
def execute(access_requester, skip_authorization: false, skip_log_audit_event: false) def execute(access_requester, skip_authorization: false, skip_log_audit_event: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_update_access_requester?(access_requester) raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_update_access_requester?(access_requester)
......
module Members module Members
class CreateService < Members::BaseService class CreateService < Members::BaseService
<<<<<<< HEAD
prepend EE::Members::CreateService prepend EE::Members::CreateService
DEFAULT_LIMIT = 100 DEFAULT_LIMIT = 100
=======
DEFAULT_LIMIT = 100
>>>>>>> upstream/master
def execute(source) def execute(source)
return error('No users specified.') if params[:user_ids].blank? return error('No users specified.') if params[:user_ids].blank?
......
module Members module Members
class DestroyService < Members::BaseService class DestroyService < Members::BaseService
<<<<<<< HEAD
prepend EE::Members::DestroyService prepend EE::Members::DestroyService
def execute(member, skip_authorization: false) def execute(member, skip_authorization: false)
...@@ -11,6 +12,17 @@ module Members ...@@ -11,6 +12,17 @@ module Members
unassign_issues_and_merge_requests(member) unless member.invite? unassign_issues_and_merge_requests(member) unless member.invite?
member.notification_setting&.destroy member.notification_setting&.destroy
=======
def execute(member, skip_authorization: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member)
return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
Member.transaction do
unassign_issues_and_merge_requests(member) unless member.invite?
member.notification_setting&.destroy
>>>>>>> upstream/master
member.destroy member.destroy
end end
...@@ -37,7 +49,43 @@ module Members ...@@ -37,7 +49,43 @@ module Members
:destroy_project_member :destroy_project_member
else else
raise "Unknown member type: #{member}!" raise "Unknown member type: #{member}!"
<<<<<<< HEAD
end
end
def unassign_issues_and_merge_requests(member)
if member.is_a?(GroupMember)
issues = Issue.unscoped.select(1)
.joins(:project)
.where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id)
# DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
IssueAssignee.unscoped
.where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
.delete_all
MergeRequestsFinder.new(current_user, group_id: member.source_id, assignee_id: member.user_id)
.execute
.update_all(assignee_id: nil)
else
project = member.source
# SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X
issues = Issue.unscoped.select(1)
.where('issues.id = issue_assignees.issue_id')
.where(project_id: project.id)
# DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
IssueAssignee.unscoped
.where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
.delete_all
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
=======
>>>>>>> upstream/master
end end
member.user.invalidate_cache_counts
end end
def unassign_issues_and_merge_requests(member) def unassign_issues_and_merge_requests(member)
......
module Members module Members
class UpdateService < Members::BaseService class UpdateService < Members::BaseService
<<<<<<< HEAD
prepend EE::Members::UpdateService prepend EE::Members::UpdateService
=======
>>>>>>> upstream/master
# returns the updated member # returns the updated member
def execute(member, permission: :update) def execute(member, permission: :update)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, action_member_permission(permission, member), member) raise Gitlab::Access::AccessDeniedError unless can?(current_user, action_member_permission(permission, member), member)
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('profile')
- page_title "Account" - page_title "Account"
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
- if current_user.ldap_user? - if current_user.ldap_user?
.alert.alert-info .alert.alert-info
......
- page_title "Authentication log" - page_title "Authentication log"
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default .row.prepend-top-default
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
......
- page_title 'Chat' - page_title 'Chat'
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default .row.prepend-top-default
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
......
- page_title "Emails" - page_title "Emails"
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default .row.prepend-top-default
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
......
- page_title "GPG Keys" - page_title "GPG Keys"
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default .row.prepend-top-default
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
......
- page_title "SSH Keys" - page_title "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default .row.prepend-top-default
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
......
...@@ -2,5 +2,4 @@ ...@@ -2,5 +2,4 @@
- breadcrumb_title @key.title - breadcrumb_title @key.title
- page_title @key.title, "SSH Keys" - page_title @key.title, "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
= render "key_details" = render "key_details"
- page_title "Notifications" - page_title "Notifications"
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
%div %div
- if @user.errors.any? - if @user.errors.any?
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
- page_title "Personal Access Tokens" - page_title "Personal Access Tokens"
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default .row.prepend-top-default
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
......
- page_title 'Preferences' - page_title 'Preferences'
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
= render "profiles/preferences/ide", f: f = render "profiles/preferences/ide", f: f
......
- breadcrumb_title "Edit Profile" - breadcrumb_title "Edit Profile"
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f| = bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f|
= form_errors(@user) = form_errors(@user)
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
- add_to_breadcrumbs("Two-Factor Authentication", profile_account_path) - add_to_breadcrumbs("Two-Factor Authentication", profile_account_path)
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
- if inject_u2f_api? - if inject_u2f_api?
......
...@@ -29,8 +29,11 @@ ...@@ -29,8 +29,11 @@
window.gl.mrWidgetData.enable_squash_before_merge = '#{@merge_request.project.feature_available?(:merge_request_squash)}' === 'true'; window.gl.mrWidgetData.enable_squash_before_merge = '#{@merge_request.project.feature_available?(:merge_request_squash)}' === 'true';
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}'; window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
<<<<<<< HEAD
#js-vue-mr-widget.mr-widget #js-vue-mr-widget.mr-widget
=======
>>>>>>> upstream/master
.content-block.content-block-small.emoji-list-container .content-block.content-block-small.emoji-list-container
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
......
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
$("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}"));
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js') = page_specific_javascript_tag('lib/ace.js')
= webpack_bundle_tag('snippet')
.snippet-form-holder .snippet-form-holder
= form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit common-note-form" } do |f| = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit common-note-form" } do |f|
......
---
title: Clear the Labels dropdown search filter after a selection is made
merge_request: 17393
author: Andrew Torres
type: changed
---
title: Keep link when redacting unauthorized object links
merge_request:
author:
type: fixed
---
title: Enables eslint in codeclimate job
merge_request: 17392
author:
type: other
---
title: Add catch-up background migration to migrate pipeline stages
merge_request: 15741
author:
type: performance
...@@ -40,6 +40,7 @@ function generateEntries() { ...@@ -40,6 +40,7 @@ function generateEntries() {
pageEntries.forEach(( path ) => generateAutoEntries(path)); pageEntries.forEach(( path ) => generateAutoEntries(path));
<<<<<<< HEAD
// EE-specific auto entries // EE-specific auto entries
const eePageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'ee/app/assets/javascripts') }); const eePageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'ee/app/assets/javascripts') });
eePageEntries.forEach(( path ) => generateAutoEntries(path, 'ee')); eePageEntries.forEach(( path ) => generateAutoEntries(path, 'ee'));
...@@ -47,6 +48,10 @@ function generateEntries() { ...@@ -47,6 +48,10 @@ function generateEntries() {
autoEntriesCount = Object.keys(autoEntries).length; autoEntriesCount = Object.keys(autoEntries).length;
=======
autoEntriesCount = Object.keys(autoEntries).length;
>>>>>>> upstream/master
const manualEntries = { const manualEntries = {
balsamiq_viewer: './blob/balsamiq_viewer.js', balsamiq_viewer: './blob/balsamiq_viewer.js',
common: './commons/index.js', common: './commons/index.js',
...@@ -60,12 +65,14 @@ function generateEntries() { ...@@ -60,12 +65,14 @@ function generateEntries() {
notebook_viewer: './blob/notebook_viewer.js', notebook_viewer: './blob/notebook_viewer.js',
pdf_viewer: './blob/pdf_viewer.js', pdf_viewer: './blob/pdf_viewer.js',
pipelines_details: './pipelines/pipeline_details_bundle.js', pipelines_details: './pipelines/pipeline_details_bundle.js',
profile: './profile/profile_bundle.js',
project_import_gl: './projects/project_import_gitlab_project.js', project_import_gl: './projects/project_import_gitlab_project.js',
protected_branches: './protected_branches', protected_branches: './protected_branches',
protected_tags: './protected_tags', protected_tags: './protected_tags',
registry_list: './registry/index.js', registry_list: './registry/index.js',
<<<<<<< HEAD
snippet: './snippet/snippet_bundle.js', snippet: './snippet/snippet_bundle.js',
=======
>>>>>>> upstream/master
sketch_viewer: './blob/sketch_viewer.js', sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js', stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js', terminal: './terminal/terminal_bundle.js',
...@@ -82,6 +89,7 @@ function generateEntries() { ...@@ -82,6 +89,7 @@ function generateEntries() {
test: './test.js', test: './test.js',
u2f: ['vendor/u2f'], u2f: ['vendor/u2f'],
webpack_runtime: './webpack.js', webpack_runtime: './webpack.js',
<<<<<<< HEAD
// EE-only // EE-only
add_gitlab_slack_application: 'ee/add_gitlab_slack_application/index.js', add_gitlab_slack_application: 'ee/add_gitlab_slack_application/index.js',
...@@ -98,6 +106,8 @@ function generateEntries() { ...@@ -98,6 +106,8 @@ function generateEntries() {
service_desk: 'ee/projects/settings_service_desk/service_desk_bundle.js', service_desk: 'ee/projects/settings_service_desk/service_desk_bundle.js',
service_desk_issues: 'ee/service_desk_issues/index.js', service_desk_issues: 'ee/service_desk_issues/index.js',
roadmap: 'ee/roadmap', roadmap: 'ee/roadmap',
=======
>>>>>>> upstream/master
}; };
return Object.assign(manualEntries, autoEntries); return Object.assign(manualEntries, autoEntries);
...@@ -389,7 +399,11 @@ if (IS_DEV_SERVER) { ...@@ -389,7 +399,11 @@ if (IS_DEV_SERVER) {
callback(); callback();
}) })
}, },
<<<<<<< HEAD
}, },
=======
}
>>>>>>> upstream/master
); );
if (DEV_SERVER_LIVERELOAD) { if (DEV_SERVER_LIVERELOAD) {
config.plugins.push(new webpack.HotModuleReplacementPlugin()); config.plugins.push(new webpack.HotModuleReplacementPlugin());
......
class AddTmpPartialNullIndexToBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL',
name: 'tmp_id_partial_null_index')
end
def down
remove_concurrent_index_by_name(:ci_builds, 'tmp_id_partial_null_index')
end
end
class ScheduleBuildStageMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'MigrateBuildStage'.freeze
BATCH_SIZE = 500
disable_ddl_transaction!
class Build < ActiveRecord::Base
include EachBatch
self.table_name = 'ci_builds'
end
def up
disable_statement_timeout
Build.where('stage_id IS NULL').tap do |relation|
queue_background_migration_jobs_by_range_at_intervals(relation,
MIGRATION,
5.minutes,
batch_size: BATCH_SIZE)
end
end
def down
# noop
end
end
class RemoveTmpPartialNullIndexFromBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
remove_concurrent_index_by_name(:ci_builds, 'tmp_id_partial_null_index')
end
def down
add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL',
name: 'tmp_id_partial_null_index')
end
end
...@@ -174,7 +174,9 @@ module Banzai ...@@ -174,7 +174,9 @@ module Banzai
title = object_link_title(object) title = object_link_title(object)
klass = reference_class(object_sym) klass = reference_class(object_sym)
data = data_attributes_for(link_content || match, parent, object, link: !!link_content) data = data_attributes_for(link_content || match, parent, object,
link_content: !!link_content,
link_reference: link_reference)
url = url =
if matches.names.include?("url") && matches[:url] if matches.names.include?("url") && matches[:url]
...@@ -194,10 +196,11 @@ module Banzai ...@@ -194,10 +196,11 @@ module Banzai
end end
end end
def data_attributes_for(text, project, object, link: false) def data_attributes_for(text, project, object, link_content: false, link_reference: false)
data_attribute( data_attribute(
original: text, original: text,
link: link, link: link_content,
link_reference: link_reference,
project: project.id, project: project.id,
object_sym => object.id object_sym => object.id
) )
......
...@@ -42,16 +42,33 @@ module Banzai ...@@ -42,16 +42,33 @@ module Banzai
next if visible.include?(node) next if visible.include?(node)
doc_data[:visible_reference_count] -= 1 doc_data[:visible_reference_count] -= 1
# The reference should be replaced by the original link's content, redacted_content = redacted_node_content(node)
# which is not always the same as the rendered one. node.replace(redacted_content)
content = node.attr('data-original') || node.inner_html
node.replace(content)
end end
end end
metadata metadata
end end
# Return redacted content of given node as either the original link (<a> tag),
# the original content (text), or the inner HTML of the node.
#
def redacted_node_content(node)
original_content = node.attr('data-original')
link_reference = node.attr('data-link-reference')
# Build the raw <a> tag just with a link as href and content if
# it's originally a link pattern. We shouldn't return a plain text href.
original_link =
if link_reference == 'true' && href = original_content
%(<a href="#{href}">#{href}</a>)
end
# The reference should be replaced by the original link's content,
# which is not always the same as the rendered one.
original_link || original_content || node.inner_html
end
def redact_cross_project_references(documents) def redact_cross_project_references(documents)
extractor = Banzai::IssuableExtractor.new(project, user) extractor = Banzai::IssuableExtractor.new(project, user)
issuables = extractor.extract(documents) issuables = extractor.extract(documents)
......
# frozen_string_literal: true
# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class MigrateBuildStage
module Migratable
class Stage < ActiveRecord::Base
self.table_name = 'ci_stages'
end
class Build < ActiveRecord::Base
self.table_name = 'ci_builds'
def ensure_stage!(attempts: 2)
find_stage || create_stage!
rescue ActiveRecord::RecordNotUnique
retry if (attempts -= 1) > 0
raise
end
def find_stage
Stage.find_by(name: self.stage || 'test',
pipeline_id: self.commit_id,
project_id: self.project_id)
end
def create_stage!
Stage.create!(name: self.stage || 'test',
pipeline_id: self.commit_id,
project_id: self.project_id)
end
end
end
def perform(start_id, stop_id)
stages = Migratable::Build.where('stage_id IS NULL')
.where('id BETWEEN ? AND ?', start_id, stop_id)
.map { |build| build.ensure_stage! }
.compact.map(&:id)
MigrateBuildStageIdReference.new.perform(start_id, stop_id)
MigrateStageStatus.new.perform(stages.min, stages.max)
end
end
end
end
...@@ -193,6 +193,18 @@ describe 'New/edit issue', :js do ...@@ -193,6 +193,18 @@ describe 'New/edit issue', :js do
expect(find('.js-label-select')).to have_content('Labels') expect(find('.js-label-select')).to have_content('Labels')
end end
it 'clears label search input field when a label is selected' do
click_button 'Labels'
page.within '.dropdown-menu-labels' do
search_field = find('input[type="search"]')
search_field.set(label2.title)
click_link label2.title
expect(search_field.value).to eq ''
end
end
it 'correctly updates the selected user when changing assignee' do it 'correctly updates the selected user when changing assignee' do
click_button 'Unassigned' click_button 'Unassigned'
......
...@@ -40,6 +40,16 @@ describe Banzai::Redactor do ...@@ -40,6 +40,16 @@ describe Banzai::Redactor do
expect(doc.to_html).to eq(original_content) expect(doc.to_html).to eq(original_content)
end end
end end
it 'returns <a> tag with original href if it is originally a link reference' do
href = 'http://localhost:3000'
doc = Nokogiri::HTML
.fragment("<a class='gfm' data-reference-type='issue' data-original=#{href} data-link-reference='true'>#{href}</a>")
redactor.redact([doc])
expect(doc.to_html).to eq('<a href="http://localhost:3000">http://localhost:3000</a>')
end
end end
context 'when project is in pending delete' do context 'when project is in pending delete' do
......
require 'spec_helper'
describe Gitlab::BackgroundMigration::MigrateBuildStage, :migration, schema: 20180212101928 do
let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }
let(:jobs) { table(:ci_builds) }
STATUSES = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
before do
projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce')
pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
jobs.create!(id: 1, commit_id: 1, project_id: 123,
stage_idx: 2, stage: 'build', status: :success)
jobs.create!(id: 2, commit_id: 1, project_id: 123,
stage_idx: 2, stage: 'build', status: :success)
jobs.create!(id: 3, commit_id: 1, project_id: 123,
stage_idx: 1, stage: 'test', status: :failed)
jobs.create!(id: 4, commit_id: 1, project_id: 123,
stage_idx: 1, stage: 'test', status: :success)
jobs.create!(id: 5, commit_id: 1, project_id: 123,
stage_idx: 3, stage: 'deploy', status: :pending)
jobs.create!(id: 6, commit_id: 1, project_id: 123,
stage_idx: 3, stage: nil, status: :pending)
end
it 'correctly migrates builds stages' do
expect(stages.count).to be_zero
described_class.new.perform(1, 6)
expect(stages.count).to eq 3
expect(stages.all.pluck(:name)).to match_array %w[test build deploy]
expect(jobs.where(stage_id: nil)).to be_one
expect(jobs.find_by(stage_id: nil).id).to eq 6
expect(stages.all.pluck(:status)).to match_array [STATUSES[:success],
STATUSES[:failed],
STATUSES[:pending]]
end
it 'recovers from unique constraint violation only twice' do
allow(described_class::Migratable::Stage)
.to receive(:find_by).and_return(nil)
expect(described_class::Migratable::Stage)
.to receive(:find_by).exactly(3).times
expect { described_class.new.perform(1, 6) }
.to raise_error ActiveRecord::RecordNotUnique
end
end
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180212101928_schedule_build_stage_migration')
describe ScheduleBuildStageMigration, :migration do
let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }
let(:jobs) { table(:ci_builds) }
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce')
pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
stages.create!(id: 1, project_id: 123, pipeline_id: 1, name: 'test')
jobs.create!(id: 11, commit_id: 1, project_id: 123, stage_id: nil)
jobs.create!(id: 206, commit_id: 1, project_id: 123, stage_id: nil)
jobs.create!(id: 3413, commit_id: 1, project_id: 123, stage_id: nil)
jobs.create!(id: 4109, commit_id: 1, project_id: 123, stage_id: 1)
end
it 'schedules delayed background migrations in batches in bulk' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 11, 11)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 206, 206)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, 3413, 3413)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
end
end
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