Commit a50741de authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ce-com/master' into ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 251d0116 56ea45ef
...@@ -5,7 +5,7 @@ require: ...@@ -5,7 +5,7 @@ require:
inherit_from: .rubocop_todo.yml inherit_from: .rubocop_todo.yml
AllCops: AllCops:
TargetRubyVersion: 2.1 TargetRubyVersion: 2.3
# Cop names are not d§splayed in offense messages by default. Change behavior # Cop names are not d§splayed in offense messages by default. Change behavior
# by overriding DisplayCopNames, or by giving the -D/--display-cop-names # by overriding DisplayCopNames, or by giving the -D/--display-cop-names
# option. # option.
......
...@@ -67,16 +67,8 @@ ...@@ -67,16 +67,8 @@
Build.prototype.initSidebar = function() { Build.prototype.initSidebar = function() {
this.$sidebar = $('.js-build-sidebar'); this.$sidebar = $('.js-build-sidebar');
this.sidebarTranslationLimits = {
min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
};
this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight();
this.$sidebar.css({
top: this.sidebarTranslationLimits.max
});
this.$sidebar.niceScroll(); this.$sidebar.niceScroll();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this));
}; };
Build.prototype.location = function() { Build.prototype.location = function() {
...@@ -231,14 +223,6 @@ ...@@ -231,14 +223,6 @@
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
}; };
Build.prototype.translateSidebar = function(e) {
var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop);
if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
this.$sidebar.css({
top: newPosition
});
};
Build.prototype.toggleSidebar = function(shouldHide) { Build.prototype.toggleSidebar = function(shouldHide) {
var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
this.$buildScroll.toggleClass('sidebar-expanded', shouldShow) this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
...@@ -285,7 +269,7 @@ ...@@ -285,7 +269,7 @@
e.preventDefault(); e.preventDefault();
$currentTarget = $(e.currentTarget); $currentTarget = $(e.currentTarget);
$.scrollTo($currentTarget.attr('href'), { $.scrollTo($currentTarget.attr('href'), {
offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()) offset: 0
}); });
}; };
......
...@@ -181,7 +181,7 @@ const Vue = require('vue'); ...@@ -181,7 +181,7 @@ const Vue = require('vue');
} }
$.scrollTo($target, { $.scrollTo($target, {
offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()) offset: 0
}); });
} }
}, },
......
...@@ -72,27 +72,18 @@ ...@@ -72,27 +72,18 @@
// This is required to handle non-unicode characters in hash // This is required to handle non-unicode characters in hash
hash = decodeURIComponent(hash); hash = decodeURIComponent(hash);
var navbar = document.querySelector('.navbar-gitlab');
var subnav = document.querySelector('.layout-nav');
var fixedTabs = document.querySelector('.js-tabs-affix');
var adjustment = 0;
if (navbar) adjustment -= navbar.offsetHeight;
if (subnav) adjustment -= subnav.offsetHeight;
// scroll to user-generated markdown anchor if we cannot find a match // scroll to user-generated markdown anchor if we cannot find a match
if (document.getElementById(hash) === null) { if (document.getElementById(hash) === null) {
var target = document.getElementById('user-content-' + hash); var target = document.getElementById('user-content-' + hash);
if (target && target.scrollIntoView) { if (target && target.scrollIntoView) {
target.scrollIntoView(true); target.scrollIntoView(true);
window.scrollBy(0, adjustment);
} }
} else { } else {
// only adjust for fixedTabs when not targeting user-generated content // only adjust for fixedTabs when not targeting user-generated content
var fixedTabs = document.querySelector('.js-tabs-affix');
if (fixedTabs) { if (fixedTabs) {
adjustment -= fixedTabs.offsetHeight; window.scrollBy(0, -fixedTabs.offsetHeight);
} }
window.scrollBy(0, adjustment);
} }
}; };
...@@ -147,12 +138,10 @@ ...@@ -147,12 +138,10 @@
gl.utils.scrollToElement = function($el) { gl.utils.scrollToElement = function($el) {
var top = $el.offset().top; var top = $el.offset().top;
gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height();
gl.navLinksHeight = gl.navLinksHeight || $('.nav-links').height();
gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height(); gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
return $('body, html').animate({ return $('body, html').animate({
scrollTop: top - (gl.navBarHeight + gl.navLinksHeight + gl.mrTabsHeight) scrollTop: top - (gl.mrTabsHeight)
}, 200); }, 200);
}; };
......
...@@ -115,8 +115,8 @@ require('./merge_request_tabs'); ...@@ -115,8 +115,8 @@ require('./merge_request_tabs');
e.preventDefault(); e.preventDefault();
textarea.val(textarea.data('messageWithDescription')); textarea.val(textarea.data('messageWithDescription'));
$('p.js-with-description-hint').hide(); $('.js-with-description-hint').hide();
$('p.js-without-description-hint').show(); $('.js-without-description-hint').show();
}); });
$(document).on('click', 'a.js-without-description-link', function(e) { $(document).on('click', 'a.js-without-description-link', function(e) {
...@@ -124,8 +124,8 @@ require('./merge_request_tabs'); ...@@ -124,8 +124,8 @@ require('./merge_request_tabs');
e.preventDefault(); e.preventDefault();
textarea.val(textarea.data('messageWithoutDescription')); textarea.val(textarea.data('messageWithoutDescription'));
$('p.js-with-description-hint').show(); $('.js-with-description-hint').show();
$('p.js-without-description-hint').hide(); $('.js-without-description-hint').hide();
}); });
}; };
......
...@@ -125,9 +125,8 @@ require('./flash'); ...@@ -125,9 +125,8 @@ require('./flash');
if (this.diffViewType() === 'parallel') { if (this.diffViewType() === 'parallel') {
this.expandViewContainer(); this.expandViewContainer();
} }
const navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo('.merge-request-details .merge-request-tabs', { $.scrollTo('.merge-request-details .merge-request-tabs', {
offset: -navBarHeight, offset: 0,
}); });
} else { } else {
this.expandView(); this.expandView();
...@@ -140,11 +139,7 @@ require('./flash'); ...@@ -140,11 +139,7 @@ require('./flash');
scrollToElement(container) { scrollToElement(container) {
if (location.hash) { if (location.hash) {
const offset = 0 - ( const offset = -$('.js-tabs-affix').outerHeight();
$('.navbar-gitlab').outerHeight() +
$('.layout-nav').outerHeight() +
$('.js-tabs-affix').outerHeight()
);
const $el = $(`${container} ${location.hash}:not(.match)`); const $el = $(`${container} ${location.hash}:not(.match)`);
if ($el.length) { if ($el.length) {
$.scrollTo($el[0], { offset }); $.scrollTo($el[0], { offset });
...@@ -330,14 +325,12 @@ require('./flash'); ...@@ -330,14 +325,12 @@ require('./flash');
if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
const $diffTabs = $('#diff-notes-app'); const $diffTabs = $('#diff-notes-app');
const $fixedNav = $('.navbar-fixed-top');
const $layoutNav = $('.layout-nav');
$tabs.off('affix.bs.affix affix-top.bs.affix') $tabs.off('affix.bs.affix affix-top.bs.affix')
.affix({ .affix({
offset: { offset: {
top: () => ( top: () => (
$diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height() $diffTabs.offset().top - $tabs.height()
), ),
}, },
}) })
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
const sidebarBreakpoint = 1024; const sidebarBreakpoint = 1024;
const pageSelector = '.page-with-sidebar'; const pageSelector = '.page-with-sidebar';
const navbarSelector = '.navbar-fixed-top'; const navbarSelector = '.navbar-gitlab';
const sidebarWrapperSelector = '.sidebar-wrapper'; const sidebarWrapperSelector = '.sidebar-wrapper';
const sidebarContentSelector = '.nav-sidebar'; const sidebarContentSelector = '.nav-sidebar';
...@@ -35,13 +35,16 @@ ...@@ -35,13 +35,16 @@
window.innerWidth >= sidebarBreakpoint && window.innerWidth >= sidebarBreakpoint &&
$(pageSelector).hasClass(expandedPageClass) $(pageSelector).hasClass(expandedPageClass)
); );
$(window).on('resize', () => this.setSidebarHeight());
$(document) $(document)
.on('click', sidebarToggleSelector, () => this.toggleSidebar()) .on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState()) .on('click', pinnedToggleSelector, () => this.togglePinnedState())
.on('click', 'html, body, a, button', (e) => this.handleClickEvent(e)) .on('click', 'html, body, a, button', (e) => this.handleClickEvent(e))
.on('DOMContentLoaded', () => this.renderState()) .on('DOMContentLoaded', () => this.renderState())
.on('scroll', () => this.setSidebarHeight())
.on('todo:toggle', (e, count) => this.updateTodoCount(count)); .on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState(); this.renderState();
this.setSidebarHeight();
} }
handleClickEvent(e) { handleClickEvent(e) {
...@@ -64,6 +67,16 @@ ...@@ -64,6 +67,16 @@
this.renderState(); this.renderState();
} }
setSidebarHeight() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const diff = $navHeight - $('body').scrollTop();
if (diff > 0) {
$('.js-right-sidebar').outerHeight($(window).height() - diff);
} else {
$('.js-right-sidebar').outerHeight('100%');
}
}
togglePinnedState() { togglePinnedState() {
this.isPinned = !this.isPinned; this.isPinned = !this.isPinned;
if (!this.isPinned) { if (!this.isPinned) {
......
...@@ -222,6 +222,10 @@ header { ...@@ -222,6 +222,10 @@ header {
float: right; float: right;
border-top: none; border-top: none;
@media (min-width: $screen-md-min) {
padding: 0;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
float: none; float: none;
} }
...@@ -272,7 +276,7 @@ header { ...@@ -272,7 +276,7 @@ header {
.header-user { .header-user {
.dropdown-menu-nav { .dropdown-menu-nav {
width: 140px; min-width: 140px;
margin-top: -5px; margin-top: -5px;
} }
} }
......
...@@ -283,10 +283,7 @@ ...@@ -283,10 +283,7 @@
} }
.layout-nav { .layout-nav {
position: fixed;
top: $header-height;
width: 100%; width: 100%;
z-index: 11;
background: $gray-light; background: $gray-light;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
...@@ -419,15 +416,20 @@ ...@@ -419,15 +416,20 @@
} }
.page-with-layout-nav { .page-with-layout-nav {
margin-top: $header-height + 2;
.right-sidebar { .right-sidebar {
top: ($header-height * 2) + 2; top: ($header-height * 2) + 2;
} }
.build-sidebar {
top: ($header-height * 3) + 3;
&.affix {
top: 0;
}
}
} }
.activities { .activities {
.nav-block { .nav-block {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
......
.page-with-sidebar { .page-with-sidebar {
padding: $header-height 0 25px; padding-bottom: 25px;
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
&.page-sidebar-pinned { &.page-sidebar-pinned {
...@@ -208,7 +208,9 @@ header.header-sidebar-pinned { ...@@ -208,7 +208,9 @@ header.header-sidebar-pinned {
padding-right: 0; padding-right: 0;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width; .content-wrapper {
padding-right: $sidebar_collapsed_width;
}
.merge-request-tabs-holder.affix { .merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width; right: $sidebar_collapsed_width;
...@@ -234,7 +236,9 @@ header.header-sidebar-pinned { ...@@ -234,7 +236,9 @@ header.header-sidebar-pinned {
} }
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding-right: $gutter_width; .content-wrapper {
padding-right: $gutter_width;
}
&:not(.with-overlay) .merge-request-tabs-holder.affix { &:not(.with-overlay) .merge-request-tabs-holder.affix {
right: $gutter_width; right: $gutter_width;
...@@ -252,4 +256,9 @@ header.header-sidebar-pinned { ...@@ -252,4 +256,9 @@ header.header-sidebar-pinned {
.right-sidebar { .right-sidebar {
border-left: 1px solid $border-color; border-left: 1px solid $border-color;
&.affix {
position: fixed;
top: 0;
}
} }
...@@ -138,6 +138,13 @@ pre { ...@@ -138,6 +138,13 @@ pre {
margin: 0; margin: 0;
} }
blockquote {
color: $gl-grayish-blue;
padding: 0 0 0 15px;
margin: 0;
border-left: 3px solid $white-dark;
}
span.highlight_word { span.highlight_word {
background-color: $highlighted-highlight-word !important; background-color: $highlighted-highlight-word !important;
} }
......
...@@ -332,12 +332,8 @@ ...@@ -332,12 +332,8 @@
.issue-boards-sidebar { .issue-boards-sidebar {
&.right-sidebar { &.right-sidebar {
top: 153px; top: 0;
bottom: 0; bottom: 0;
@media (min-width: $screen-sm-min) {
top: 220px;
}
} }
.issuable-sidebar-header { .issuable-sidebar-header {
......
...@@ -41,7 +41,6 @@ ...@@ -41,7 +41,6 @@
word-wrap: break-word; word-wrap: break-word;
.md { .md {
color: $gl-grayish-blue;
font-size: $gl-font-size; font-size: $gl-font-size;
.label { .label {
......
...@@ -189,7 +189,7 @@ ...@@ -189,7 +189,7 @@
} }
.right-sidebar { .right-sidebar {
position: fixed; position: absolute;
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
right: 0; right: 0;
......
...@@ -96,13 +96,6 @@ ...@@ -96,13 +96,6 @@
padding-right: 4px; padding-right: 4px;
} }
&.ci-success_with_warnings {
i {
color: $gl-warning;
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
flex-wrap: wrap; flex-wrap: wrap;
} }
...@@ -486,7 +479,7 @@ ...@@ -486,7 +479,7 @@
background-color: $white-light; background-color: $white-light;
&.affix { &.affix {
top: 100px; top: 0;
left: 0; left: 0;
z-index: 10; z-index: 10;
transition: right .15s; transition: right .15s;
......
...@@ -94,6 +94,10 @@ ...@@ -94,6 +94,10 @@
padding: 10px 8px; padding: 10px 8px;
} }
td.stage-cell {
padding: 10px 0;
}
.commit-link { .commit-link {
padding: 9px 8px 10px; padding: 9px 8px 10px;
} }
...@@ -291,12 +295,14 @@ ...@@ -291,12 +295,14 @@
height: 22px; height: 22px;
margin: 3px 6px 3px 0; margin: 3px 6px 3px 0;
.tooltip { // Hack to show a button tooltip inline
white-space: nowrap; button.has-tooltip + .tooltip {
min-width: 105px;
} }
.tooltip-inner { // Bootstrap way of showing the content inline for anchors.
padding: 3px 4px; a.has-tooltip {
white-space: nowrap;
} }
&:not(:last-child) { &:not(:last-child) {
......
...@@ -171,8 +171,6 @@ ...@@ -171,8 +171,6 @@
.tree-controls { .tree-controls {
float: right; float: right;
margin-top: 11px; margin-top: 11px;
position: relative;
z-index: 2;
.project-action-button { .project-action-button {
margin-left: $btn-side-margin; margin-left: $btn-side-margin;
......
...@@ -9,6 +9,28 @@ module IssuableCollections ...@@ -9,6 +9,28 @@ module IssuableCollections
private private
def issuable_meta_data(issuable_collection)
# map has to be used here since using pluck or select will
# throw an error when ordering issuables by priority which inserts
# a new order into the collection.
# We cannot use reorder to not mess up the paginated collection.
issuable_ids = issuable_collection.map(&:id)
issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
issuable_ids.each_with_object({}) do |id, issuable_meta|
downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
notes = issuable_note_count.find { |notes| notes.noteable_id == id }
issuable_meta[id] = Issuable::IssuableMeta.new(
upvotes.try(:count).to_i,
downvotes.try(:count).to_i,
notes.try(:count).to_i
)
end
end
def issues_collection def issues_collection
issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace) issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
end end
......
...@@ -9,6 +9,9 @@ module IssuesAction ...@@ -9,6 +9,9 @@ module IssuesAction
.non_archived .non_archived
.page(params[:page]) .page(params[:page])
@collection_type = "Issue"
@issuable_meta_data = issuable_meta_data(@issues)
respond_to do |format| respond_to do |format|
format.html format.html
format.atom { render layout: false } format.atom { render layout: false }
......
...@@ -7,6 +7,9 @@ module MergeRequestsAction ...@@ -7,6 +7,9 @@ module MergeRequestsAction
@merge_requests = merge_requests_collection @merge_requests = merge_requests_collection
.page(params[:page]) .page(params[:page])
@collection_type = "MergeRequest"
@issuable_meta_data = issuable_meta_data(@merge_requests)
end end
private private
......
...@@ -12,7 +12,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -12,7 +12,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id] @project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members @members = GroupMembersFinder.new(@group).execute
@members = @members.non_invite unless can?(current_user, :admin_group, @group) @members = @members.non_invite unless can?(current_user, :admin_group, @group)
@members = @members.search(params[:search]) if params[:search].present? @members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort) @members = @members.sort(@sort)
......
...@@ -61,10 +61,10 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -61,10 +61,10 @@ class Projects::BlobController < Projects::ApplicationController
end end
def destroy def destroy
create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.", create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.",
success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch), success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch),
failure_view: :show, failure_view: :show,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end end
def diff def diff
......
...@@ -23,8 +23,11 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -23,8 +23,11 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html respond_to :html
def index def index
@issues = issues_collection @collection_type = "Issue"
@issues = @issues.page(params[:page]) @issues = issues_collection
@issues = @issues.page(params[:page])
@issuable_meta_data = issuable_meta_data(@issues)
if @issues.out_of_range? && @issues.total_pages != 0 if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages)) return redirect_to url_for(params.merge(page: @issues.total_pages))
end end
......
...@@ -39,8 +39,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -39,8 +39,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts] before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
def index def index
@merge_requests = merge_requests_collection @collection_type = "MergeRequest"
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
@issuable_meta_data = issuable_meta_data(@merge_requests)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
end end
......
...@@ -87,13 +87,14 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -87,13 +87,14 @@ class Projects::WikisController < Projects::ApplicationController
def destroy def destroy
@page = @project_wiki.find_page(params[:id]) @page = @project_wiki.find_page(params[:id])
if @page if @page
@page.delete @page.delete
# Triggers repository update on secondary nodes when Geo is enabled # Triggers repository update on secondary nodes when Geo is enabled
Gitlab::Geo.notify_wiki_update(@project) if Gitlab::Geo.primary? Gitlab::Geo.notify_wiki_update(@project) if Gitlab::Geo.primary?
end end
redirect_to( redirect_to(
namespace_project_wiki_path(@project.namespace, @project, :home), namespace_project_wiki_path(@project.namespace, @project, :home),
notice: "Page was successfully deleted" notice: "Page was successfully deleted"
......
class GroupMembersFinder < Projects::ApplicationController
def initialize(group)
@group = group
end
def execute
group_members = @group.members
return group_members unless @group.parent
parents_members = GroupMember.non_request.
where(source_id: @group.ancestors.select(:id)).
where.not(user_id: @group.users.select(:id))
wheres = ["members.id IN (#{group_members.select(:id).to_sql})"]
wheres << "members.id IN (#{parents_members.select(:id).to_sql})"
GroupMember.where(wheres.join(' OR '))
end
end
...@@ -152,7 +152,7 @@ class Notify < BaseMailer ...@@ -152,7 +152,7 @@ class Notify < BaseMailer
headers['In-Reply-To'] = message_id(model) headers['In-Reply-To'] = message_id(model)
headers['References'] = message_id(model) headers['References'] = message_id(model)
headers[:subject].prepend('Re: ') if headers[:subject] headers[:subject]&.prepend('Re: ')
mail_thread(model, headers) mail_thread(model, headers)
end end
......
...@@ -129,31 +129,25 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -129,31 +129,25 @@ class ApplicationSetting < ActiveRecord::Base
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates_each :restricted_visibility_levels do |record, attr, value| validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil? value&.each do |level|
value.each do |level| unless Gitlab::VisibilityLevel.options.has_value?(level)
unless Gitlab::VisibilityLevel.options.has_value?(level) record.errors.add(attr, "'#{level}' is not a valid visibility level")
record.errors.add(attr, "'#{level}' is not a valid visibility level")
end
end end
end end
end end
validates_each :import_sources do |record, attr, value| validates_each :import_sources do |record, attr, value|
unless value.nil? value&.each do |source|
value.each do |source| unless Gitlab::ImportSources.options.has_value?(source)
unless Gitlab::ImportSources.options.has_value?(source) record.errors.add(attr, "'#{source}' is not a import source")
record.errors.add(attr, "'#{source}' is not a import source")
end
end end
end end
end end
validates_each :disabled_oauth_sign_in_sources do |record, attr, value| validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
unless value.nil? value&.each do |source|
value.each do |source| unless Devise.omniauth_providers.include?(source.to_sym)
unless Devise.omniauth_providers.include?(source.to_sym) record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
end
end end
end end
end end
...@@ -255,11 +249,11 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -255,11 +249,11 @@ class ApplicationSetting < ActiveRecord::Base
end end
def domain_whitelist_raw def domain_whitelist_raw
self.domain_whitelist.join("\n") unless self.domain_whitelist.nil? self.domain_whitelist&.join("\n")
end end
def domain_blacklist_raw def domain_blacklist_raw
self.domain_blacklist.join("\n") unless self.domain_blacklist.nil? self.domain_blacklist&.join("\n")
end end
def domain_whitelist_raw=(values) def domain_whitelist_raw=(values)
......
...@@ -16,6 +16,14 @@ class AwardEmoji < ActiveRecord::Base ...@@ -16,6 +16,14 @@ class AwardEmoji < ActiveRecord::Base
scope :downvotes, -> { where(name: DOWNVOTE_NAME) } scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) } scope :upvotes, -> { where(name: UPVOTE_NAME) }
class << self
def votes_for_collection(ids, type)
select('name', 'awardable_id', 'COUNT(*) as count').
where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids).
group('name', 'awardable_id')
end
end
def downvote? def downvote?
self.name == DOWNVOTE_NAME self.name == DOWNVOTE_NAME
end end
......
...@@ -15,6 +15,11 @@ module Issuable ...@@ -15,6 +15,11 @@ module Issuable
include Taskable include Taskable
include TimeTrackable include TimeTrackable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes and notes count for issues and merge requests
# lists avoiding n+1 queries and improving performance.
IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count)
included do included do
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description cache_markdown_field :description
...@@ -98,8 +103,8 @@ module Issuable ...@@ -98,8 +103,8 @@ module Issuable
def update_assignee_cache_counts def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist) # make sure we flush the cache for both the old *and* new assignees(if they exist)
previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
previous_assignee.update_cache_counts if previous_assignee previous_assignee&.update_cache_counts
assignee.update_cache_counts if assignee assignee&.update_cache_counts
end end
# We want to use optimistic lock for cases when only title or description are involved # We want to use optimistic lock for cases when only title or description are involved
......
...@@ -73,7 +73,7 @@ module Milestoneish ...@@ -73,7 +73,7 @@ module Milestoneish
def memoize_per_user(user, method_name) def memoize_per_user(user, method_name)
@memoized ||= {} @memoized ||= {}
@memoized[method_name] ||= {} @memoized[method_name] ||= {}
@memoized[method_name][user.try!(:id)] ||= yield @memoized[method_name][user&.id] ||= yield
end end
# override in a class that includes this module to get a faster query # override in a class that includes this module to get a faster query
......
...@@ -250,7 +250,7 @@ class Group < Namespace ...@@ -250,7 +250,7 @@ class Group < Namespace
end end
def members_with_parents def members_with_parents
GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id)) GroupMember.non_request.where(source_id: ancestors.map(&:id).push(id))
end end
def users_with_parents def users_with_parents
......
...@@ -9,7 +9,7 @@ class GroupMilestone < GlobalMilestone ...@@ -9,7 +9,7 @@ class GroupMilestone < GlobalMilestone
def self.build(group, projects, title) def self.build(group, projects, title)
super(projects, title).tap do |milestone| super(projects, title).tap do |milestone|
milestone.group = group if milestone milestone&.group = group
end end
end end
......
...@@ -48,6 +48,7 @@ class Member < ActiveRecord::Base ...@@ -48,6 +48,7 @@ class Member < ActiveRecord::Base
scope :invite, -> { where.not(invite_token: nil) } scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) } scope :non_invite, -> { where(invite_token: nil) }
scope :request, -> { where.not(requested_at: nil) } scope :request, -> { where.not(requested_at: nil) }
scope :non_request, -> { where(requested_at: nil) }
scope :has_access, -> { active.where('access_level > 0') } scope :has_access, -> { active.where('access_level > 0') }
......
...@@ -110,6 +110,12 @@ class Note < ActiveRecord::Base ...@@ -110,6 +110,12 @@ class Note < ActiveRecord::Base
Discussion.for_diff_notes(active_notes). Discussion.for_diff_notes(active_notes).
map { |d| [d.line_code, d] }.to_h map { |d| [d.line_code, d] }.to_h
end end
def count_for_collection(ids, type)
user.select('noteable_id', 'COUNT(*) as count').
group(:noteable_id).
where(noteable_type: type, noteable_id: ids)
end
end end
def searchable? def searchable?
......
...@@ -508,7 +508,7 @@ class Project < ActiveRecord::Base ...@@ -508,7 +508,7 @@ class Project < ActiveRecord::Base
def reset_cache_and_import_attrs def reset_cache_and_import_attrs
ProjectCacheWorker.perform_async(self.id) ProjectCacheWorker.perform_async(self.id)
self.import_data.destroy if !mirror? && self.import_data self.import_data&.destroy if !mirror?
end end
def import_url=(value) def import_url=(value)
......
...@@ -334,7 +334,7 @@ class User < ActiveRecord::Base ...@@ -334,7 +334,7 @@ class User < ActiveRecord::Base
def find_by_personal_access_token(token_string) def find_by_personal_access_token(token_string)
personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
personal_access_token.user if personal_access_token personal_access_token&.user
end end
# Returns a user for the given SSH key. # Returns a user for the given SSH key.
......
...@@ -69,16 +69,12 @@ class WikiPage ...@@ -69,16 +69,12 @@ class WikiPage
# The raw content of this page. # The raw content of this page.
def content def content
@attributes[:content] ||= if @page @attributes[:content] ||= @page&.text_data
@page.text_data
end
end end
# The processed/formatted content of this page. # The processed/formatted content of this page.
def formatted_content def formatted_content
@attributes[:formatted_content] ||= if @page @attributes[:formatted_content] ||= @page&.formatted_data
@page.formatted_data
end
end end
# The markup format for the page. # The markup format for the page.
......
...@@ -4,7 +4,7 @@ class CreateTagService < BaseService ...@@ -4,7 +4,7 @@ class CreateTagService < BaseService
return error('Tag name invalid') unless valid_tag return error('Tag name invalid') unless valid_tag
repository = project.repository repository = project.repository
message.strip! if message message&.strip!
new_tag = nil new_tag = nil
......
...@@ -9,7 +9,7 @@ class DeleteTagService < BaseService ...@@ -9,7 +9,7 @@ class DeleteTagService < BaseService
if repository.rm_tag(current_user, tag_name) if repository.rm_tag(current_user, tag_name)
release = project.releases.find_by(tag: tag_name) release = project.releases.find_by(tag: tag_name)
release.destroy if release release&.destroy
push_data = build_push_data(tag) push_data = build_push_data(tag)
EventCreateService.new.push(project, current_user, push_data) EventCreateService.new.push(project, current_user, push_data)
......
module Files module Files
class DeleteService < Files::BaseService class DestroyService < Files::BaseService
def commit def commit
repository.remove_file( repository.remove_file(
current_user, current_user,
......
...@@ -44,7 +44,15 @@ module Issues ...@@ -44,7 +44,15 @@ module Issues
end end
def issue_params def issue_params
@issue_params ||= issue_params_with_info_from_merge_request.merge(params.slice(:title, :description)) @issue_params ||= issue_params_with_info_from_merge_request.merge(whitelisted_issue_params)
end
def whitelisted_issue_params
if can?(current_user, :admin_issue, project)
params.slice(:title, :description, :milestone_id)
else
params.slice(:title, :description)
end
end end
end end
end end
...@@ -107,7 +107,7 @@ module Projects ...@@ -107,7 +107,7 @@ module Projects
project.push_rule = push_rule project.push_rule = push_rule
end end
@project.group.refresh_members_authorized_projects if @project.group @project.group&.refresh_members_authorized_projects
end end
def skip_wiki? def skip_wiki?
......
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } %header.navbar.navbar-gitlab{ class: nav_header_class }
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid .container-fluid
.header-content .header-content
......
- builds = @build.pipeline.builds.to_a - builds = @build.pipeline.builds.to_a
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } }
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Job Job
%strong ##{@build.id} %strong ##{@build.id}
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
- else - else
%span.api.monospace API %span.api.monospace API
- if pipeline.latest? - if pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest job for this branch' } latest %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest
- if pipeline.triggered? - if pipeline.triggered?
%span.label.label-primary triggered %span.label.label-primary triggered
- if pipeline.yaml_errors.present? - if pipeline.yaml_errors.present?
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
.btn-group.inline .btn-group.inline
- if actions.any? - if actions.any?
.btn-group .btn-group
%button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual job', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual job' } %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' }
= custom_icon('icon_play') = custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true') = icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
.col-sm-6 .col-sm-6
.nav-controls .nav-controls
= link_to @environment.external_url, class: 'btn btn-default' do
= icon('external-link')
= render 'projects/deployments/actions', deployment: @environment.last_deployment = render 'projects/deployments/actions', deployment: @environment.last_deployment
.terminal-container{ class: container_class } .terminal-container{ class: container_class }
......
...@@ -17,22 +17,7 @@ ...@@ -17,22 +17,7 @@
%li %li
= link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name") = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
- upvotes, downvotes = issue.upvotes, issue.downvotes = render 'shared/issuable_meta_data', issuable: issue
- if upvotes > 0
%li
= icon('thumbs-up')
= upvotes
- if downvotes > 0
%li
= icon('thumbs-down')
= downvotes
- note_count = issue.notes.user.count
%li
= link_to issue_path(issue, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
= icon('comments')
= note_count
.issue-info .issue-info
#{issuable_reference(issue)} &middot; #{issuable_reference(issue)} &middot;
......
...@@ -29,22 +29,7 @@ ...@@ -29,22 +29,7 @@
%li %li
= link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
- upvotes, downvotes = merge_request.upvotes, merge_request.downvotes = render 'shared/issuable_meta_data', issuable: merge_request
- if upvotes > 0
%li
= icon('thumbs-up')
= upvotes
- if downvotes > 0
%li
= icon('thumbs-down')
= downvotes
- note_count = merge_request.related_notes.user.count
%li
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
= icon('comments')
= note_count
.merge-request-info .merge-request-info
#{issuable_reference(merge_request)} &middot; #{issuable_reference(merge_request)} &middot;
......
...@@ -9,8 +9,9 @@ ...@@ -9,8 +9,9 @@
Pipeline Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status) = ci_label_for_status(status)
.mr-widget-pipeline-graph - if @pipeline.stages.any?
= render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph' .mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph'
%span %span
for for
= succeed "." do = succeed "." do
......
...@@ -17,11 +17,11 @@ ...@@ -17,11 +17,11 @@
ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
ci_message: { ci_message: {
normal: "Pipeline {{status}} for \"{{title}}\"", normal: "Pipeline {{status}} for \"{{title}}\"",
preparing: "{{status}} job for \"{{title}}\"" preparing: "{{status}} pipeline for \"{{title}}\""
}, },
ci_enable: #{@project.ci_service ? "true" : "false"}, ci_enable: #{@project.ci_service ? "true" : "false"},
ci_title: { ci_title: {
preparing: "{{status}} job", preparing: "{{status}} pipeline",
normal: "Pipeline {{status}}" normal: "Pipeline {{status}}"
}, },
ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}", ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}",
......
%h4 %h4
= icon('exclamation-triangle') = icon('exclamation-triangle')
The job for this merge request failed The pipeline for this merge request failed
%p %p
Please retry the job or push a new commit to fix the failure. Please retry the job or push a new commit to fix the failure.
...@@ -94,9 +94,8 @@ ...@@ -94,9 +94,8 @@
.form-group.project-visibility-level-holder .form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'label-light' do = f.label :visibility_level, class: 'label-light' do
Visibility Level Visibility Level
= link_to "(?)", help_page_path("public_access/public_access") = link_to icon('question-circle'), help_page_path("public_access/public_access")
= render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project, with_label: false
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
In the In the
%code .gitlab-ci.yml %code .gitlab-ci.yml
of another project, include the following snippet. of another project, include the following snippet.
The project will be rebuilt at the end of the job. The project will be rebuilt at the end of the pipeline.
%pre %pre
:plain :plain
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
%p.light %p.light
Add Add
%code variables[VARIABLE]=VALUE %code variables[VARIABLE]=VALUE
to an API request. Variable values can be used to distinguish between triggered jobs and normal jobs. to an API request. Variable values can be used to distinguish between triggered pipelines and normal pipelines.
With cURL: With cURL:
......
%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar %aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.block.wiki-sidebar-header.append-bottom-default .block.wiki-sidebar-header.append-bottom-default
%a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
= icon('angle-double-right') = icon('angle-double-right')
......
...@@ -17,9 +17,9 @@ ...@@ -17,9 +17,9 @@
Try to keep the first line under 52 characters Try to keep the first line under 52 characters
and the others under 72. and the others under 72.
- if descriptions.present? - if descriptions.present?
%p.hint.js-with-description-hint .hint.js-with-description-hint
= link_to "#", class: "js-with-description-link" do = link_to "#", class: "js-with-description-link" do
Include description in commit message Include description in commit message
%p.hint.js-without-description-hint.hide .hint.js-without-description-hint.hide
= link_to "#", class: "js-without-description-link" do = link_to "#", class: "js-without-description-link" do
Don't include description in commit message Don't include description in commit message
- note_count = @issuable_meta_data[issuable.id].notes_count
- issue_votes = @issuable_meta_data[issuable.id]
- upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes
- issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes')
- if upvotes > 0
%li
= icon('thumbs-up')
= upvotes
- if downvotes > 0
%li
= icon('thumbs-down')
= downvotes
%li
= link_to issuable_url, class: ('no-comments' if note_count.zero?) do
= icon('comments')
= note_count
- with_label = local_assigns.fetch(:with_label, true)
.form-group.project-visibility-level-holder .form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'control-label' do - if with_label
Visibility Level = f.label :visibility_level, class: 'control-label' do
= link_to icon('question-circle'), help_page_path("public_access/public_access") Visibility Level
.col-sm-10 = link_to icon('question-circle'), help_page_path("public_access/public_access")
%div{ :class => ("col-sm-10" if with_label) }
- if can_change_visibility_level - if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
- else - else
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('issuable') = page_specific_javascript_bundle_tag('issuable')
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } %aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar .issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header .block.issuable-sidebar-header
......
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
%label.label.label-danger %label.label.label-danger
%strong Blocked %strong Blocked
- if source.instance_of?(Group) && !@group - if source.instance_of?(Group) && source != @group
&middot; &middot;
= link_to source.name, source, class: "member-group-link" = link_to source.full_name, source, class: "member-group-link"
.hidden-xs.cgray .hidden-xs.cgray
- if member.request? - if member.request?
...@@ -47,9 +47,10 @@ ...@@ -47,9 +47,10 @@
= link_to member.created_by.name, user_path(member.created_by) = link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at) = time_ago_with_tooltip(member.created_at)
- if show_roles - if show_roles
- current_resource = @project || @group
.controls.member-controls .controls.member-controls
= render 'shared/members/ee/ldap_tag', can_override: can_override_member, visible: false = render 'shared/members/ee/ldap_tag', can_override: can_override_member, visible: false
- if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project) - if show_controls && member.source == current_resource
- if user != current_user - if user != current_user
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= f.hidden_field :access_level = f.hidden_field :access_level
......
...@@ -16,6 +16,6 @@ class AuthorizedProjectsWorker ...@@ -16,6 +16,6 @@ class AuthorizedProjectsWorker
def perform(user_id) def perform(user_id)
user = User.find_by(id: user_id) user = User.find_by(id: user_id)
user.refresh_authorized_projects if user user&.refresh_authorized_projects
end end
end end
---
title: Optionally make users created via the API set their password
merge_request: 8957
author: Joost Rijneveld
---
title: Added external environment link to web terminal view
merge_request: 8303
author:
---
title: Fixes markdown in activity-feed is gray
merge_request: 9179
author:
---
title: Fixes FE Doc broken link
merge_request: 9120
author:
---
title: Fix tooltips in mini pipeline graph
merge_request:
author:
---
title: Show pipeline graph in MR widget if there are any stages
merge_request:
author:
---
title: Fix icon colors in merge request widget mini graph
merge_request:
author:
---
title: Fix MR widget jump
merge_request: 9146
author:
---
title: Improve blockquote formatting in notification emails
merge_request:
author:
---
title: Fix job to pipeline renaming
merge_request: 9147
author:
---
title: fix milestone does not automatically assign when create issue from milestone
merge_request:
author:
---
title: Removed duplicate "Visibility Level" label on New Project page
merge_request:
author: Robert Marcano
---
title: Gather issuable metadata to avoid n+1 queries on index view
merge_request:
author:
---
title: Rename Files::DeleteService to Files::DestroyService
merge_request: 9110
author: dixpac
---
title: Add index to ci_trigger_requests for commit_id
merge_request:
author:
---
title: Remove fixed positioning from top nav
merge_request: !7547
author:
...@@ -4,6 +4,12 @@ module RspecProfilingConnection ...@@ -4,6 +4,12 @@ module RspecProfilingConnection
end end
end end
module RspecProfilingGitBranchCi
def branch
ENV['CI_BUILD_REF_NAME'] || super
end
end
if Rails.env.test? if Rails.env.test?
RspecProfiling.configure do |config| RspecProfiling.configure do |config|
if ENV['RSPEC_PROFILING_POSTGRES_URL'] if ENV['RSPEC_PROFILING_POSTGRES_URL']
...@@ -11,4 +17,6 @@ if Rails.env.test? ...@@ -11,4 +17,6 @@ if Rails.env.test?
config.collector = RspecProfiling::Collectors::PSQL config.collector = RspecProfiling::Collectors::PSQL
end end
end end
RspecProfiling::VCS::Git.prepend(RspecProfilingGitBranchCi) if ENV.has_key?('CI')
end end
...@@ -7,7 +7,7 @@ class AddGroupIdToLabels < ActiveRecord::Migration ...@@ -7,7 +7,7 @@ class AddGroupIdToLabels < ActiveRecord::Migration
def change def change
add_column :labels, :group_id, :integer add_column :labels, :group_id, :integer
add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
add_concurrent_index :labels, :group_id add_concurrent_index :labels, :group_id
end end
end end
...@@ -28,6 +28,6 @@ class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration ...@@ -28,6 +28,6 @@ class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
def change def change
add_column :merge_request_metrics, :pipeline_id, :integer add_column :merge_request_metrics, :pipeline_id, :integer
add_concurrent_index :merge_request_metrics, :pipeline_id add_concurrent_index :merge_request_metrics, :pipeline_id
add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
end end
end end
...@@ -5,7 +5,7 @@ class AddProjectIdToSubscriptions < ActiveRecord::Migration ...@@ -5,7 +5,7 @@ class AddProjectIdToSubscriptions < ActiveRecord::Migration
def up def up
add_column :subscriptions, :project_id, :integer add_column :subscriptions, :project_id, :integer
add_foreign_key :subscriptions, :projects, column: :project_id, on_delete: :cascade add_foreign_key :subscriptions, :projects, column: :project_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
end end
def down def down
......
class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
add_concurrent_index :ci_trigger_requests, :commit_id
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# 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.
ActiveRecord::Schema.define(version: 20170207150212) do ActiveRecord::Schema.define(version: 20170210075922) do
# 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"
...@@ -119,7 +119,6 @@ ActiveRecord::Schema.define(version: 20170207150212) do ...@@ -119,7 +119,6 @@ ActiveRecord::Schema.define(version: 20170207150212) do
t.boolean "plantuml_enabled" t.boolean "plantuml_enabled"
t.integer "shared_runners_minutes", default: 0, null: false t.integer "shared_runners_minutes", default: 0, null: false
t.integer "repository_size_limit", limit: 8, default: 0 t.integer "repository_size_limit", limit: 8, default: 0
t.integer "max_pages_size", default: 100, null: false
t.integer "terminal_max_session_time", default: 0, null: false t.integer "terminal_max_session_time", default: 0, null: false
end end
...@@ -407,6 +406,8 @@ ActiveRecord::Schema.define(version: 20170207150212) do ...@@ -407,6 +406,8 @@ ActiveRecord::Schema.define(version: 20170207150212) do
t.integer "commit_id" t.integer "commit_id"
end end
add_index "ci_trigger_requests", ["commit_id"], name: "index_ci_trigger_requests_on_commit_id", using: :btree
create_table "ci_triggers", force: :cascade do |t| create_table "ci_triggers", force: :cascade do |t|
t.string "token" t.string "token"
t.integer "project_id" t.integer "project_id"
......
...@@ -218,7 +218,7 @@ Parameters: ...@@ -218,7 +218,7 @@ Parameters:
## User creation ## User creation
Creates a new user. Note only administrators can create new users. Creates a new user. Note only administrators can create new users. Either `password` or `reset_password` should be specified (`reset_password` takes priority).
``` ```
POST /users POST /users
...@@ -227,7 +227,8 @@ POST /users ...@@ -227,7 +227,8 @@ POST /users
Parameters: Parameters:
- `email` (required) - Email - `email` (required) - Email
- `password` (required) - Password - `password` (optional) - Password
- `reset_password` (optional) - Send user password reset link - true or false(default)
- `username` (required) - Username - `username` (required) - Username
- `name` (required) - Name - `name` (required) - Name
- `skype` (optional) - Skype ID - `skype` (optional) - Skype ID
......
...@@ -50,7 +50,7 @@ Let's look into each of them: ...@@ -50,7 +50,7 @@ Let's look into each of them:
This is the index file of your new feature. This is where the root Vue instance This is the index file of your new feature. This is where the root Vue instance
of the new feature should be. of the new feature should be.
Don't forget to follow [these steps.][page-specific-javascript] Don't forget to follow [these steps.][page_specific_javascript]
**A folder for Components** **A folder for Components**
...@@ -250,23 +250,17 @@ information. ...@@ -250,23 +250,17 @@ information.
### Running frontend tests ### Running frontend tests
`rake teaspoon` runs the frontend-only (JavaScript) tests. `rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks: It consists of two subtasks:
- `rake teaspoon:fixtures` (re-)generates fixtures - `rake karma:fixtures` (re-)generates fixtures
- `rake teaspoon:tests` actually executes the tests - `rake karma:tests` actually executes the tests
As long as the fixtures don't change, `rake teaspoon:tests` is sufficient As long as the fixtures don't change, `rake karma:tests` is sufficient
(and saves you some time). (and saves you some time).
If you need to debug your tests and/or application code while they're
running, navigate to [localhost:3000/teaspoon](http://localhost:3000/teaspoon)
in your browser, open DevTools, and run tests for individual files by clicking
on them. This is also much faster than setting up and running tests from the
command line.
Please note: Not all of the frontend fixtures are generated. Some are still static Please note: Not all of the frontend fixtures are generated. Some are still static
files. These will not be touched by `rake teaspoon:fixtures`. files. These will not be touched by `rake karma:fixtures`.
## Design Patterns ## Design Patterns
...@@ -323,54 +317,13 @@ gl.MyThing = MyThing; ...@@ -323,54 +317,13 @@ gl.MyThing = MyThing;
For our currently-supported browsers, see our [requirements][requirements]. For our currently-supported browsers, see our [requirements][requirements].
[rails]: http://rubyonrails.org/
[haml]: http://haml.info/
[hamlit]: https://github.com/k0kubun/hamlit
[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
[scss]: http://sass-lang.com/
[es6]: https://babeljs.io/
[sprockets]: https://github.com/rails/sprockets
[jquery]: https://jquery.com/
[vue]: http://vuejs.org/
[vue-docs]: http://vuejs.org/guide/index.html
[web-page-test]: http://www.webpagetest.org/
[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
[browser-diet]: https://browserdiet.com/
[d3]: https://d3js.org/
[chartjs]: http://www.chartjs.org/
[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
[observatory-cli]: https://github.com/mozilla/http-observatory-cli
[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
[secure_headers]: https://github.com/twitter/secureheaders
[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
[github-eng-sri]: http://githubengineering.com/subresource-integrity/
[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
[scss-style-guide]: scss_styleguide.md
[requirements]: ../install/requirements.md#supported-web-browsers
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[vue-resource-repo]: https://github.com/pagekit/vue-resource
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
## Gotchas ## Gotchas
### Spec errors due to use of ES6 features in `.js` files ### Spec errors due to use of ES6 features in `.js` files
If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being
thrown in Teaspoon, Spinach, or Rspec tests but can't reproduce them manually, thrown in Karma, Spinach, or Rspec tests but can't reproduce them manually,
you may have included `ES6`-style JavaScript in files that don't have the you may have included `ES6`-style JavaScript in files that don't have the
`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file
you're working in (`git mv <file.js> <file.js.es6>`). you're working in (`git mv <file.js> <file.js.es6>`).
...@@ -438,3 +391,46 @@ Scenario: Developer can approve merge request ...@@ -438,3 +391,46 @@ Scenario: Developer can approve merge request
Then I should see approved merge request "Bug NS-04" Then I should see approved merge request "Bug NS-04"
``` ```
[rails]: http://rubyonrails.org/
[haml]: http://haml.info/
[hamlit]: https://github.com/k0kubun/hamlit
[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
[scss]: http://sass-lang.com/
[es6]: https://babeljs.io/
[sprockets]: https://github.com/rails/sprockets
[jquery]: https://jquery.com/
[vue]: http://vuejs.org/
[vue-docs]: http://vuejs.org/guide/index.html
[web-page-test]: http://www.webpagetest.org/
[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
[browser-diet]: https://browserdiet.com/
[d3]: https://d3js.org/
[chartjs]: http://www.chartjs.org/
[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
[observatory-cli]: https://github.com/mozilla/http-observatory-cli
[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
[secure_headers]: https://github.com/twitter/secureheaders
[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
[github-eng-sri]: http://githubengineering.com/subresource-integrity/
[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
[scss-style-guide]: scss_styleguide.md
[requirements]: ../install/requirements.md#supported-web-browsers
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[vue-resource-repo]: https://github.com/pagekit/vue-resource
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
...@@ -17,14 +17,14 @@ Note: `db:setup` calls `db:seed` but this does nothing. ...@@ -17,14 +17,14 @@ Note: `db:setup` calls `db:seed` but this does nothing.
In order to run the test you can use the following commands: In order to run the test you can use the following commands:
- `rake spinach` to run the spinach suite - `rake spinach` to run the spinach suite
- `rake spec` to run the rspec suite - `rake spec` to run the rspec suite
- `rake teaspoon` to run the teaspoon test suite - `rake karma` to run the karma test suite
- `rake gitlab:test` to run all the tests - `rake gitlab:test` to run all the tests
Note: Both `rake spinach` and `rake spec` takes significant time to pass. Note: Both `rake spinach` and `rake spec` takes significant time to pass.
Instead of running full test suite locally you can save a lot of time by running Instead of running full test suite locally you can save a lot of time by running
a single test or directory related to your changes. After you submit merge request a single test or directory related to your changes. After you submit merge request
CI will run full test suite for you. Green CI status in the merge request means CI will run full test suite for you. Green CI status in the merge request means
full test suite is passed. full test suite is passed.
Note: You can't run `rspec .` since this will try to run all the `_spec.rb` Note: You can't run `rspec .` since this will try to run all the `_spec.rb`
files it can find, also the ones in `/tmp` files it can find, also the ones in `/tmp`
......
...@@ -31,9 +31,8 @@ GitLab uses [factory_girl] as a test fixture replacement. ...@@ -31,9 +31,8 @@ GitLab uses [factory_girl] as a test fixture replacement.
## JavaScript ## JavaScript
GitLab uses [Teaspoon] to run its [Jasmine] JavaScript specs. They can be run on GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on
the command line via `bundle exec teaspoon`, or via a web browser at the command line via `bundle exec karma`.
`http://localhost:3000/teaspoon` when the Rails server is running.
- JavaScript tests live in `spec/javascripts/`, matching the folder structure of - JavaScript tests live in `spec/javascripts/`, matching the folder structure of
`app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding
...@@ -51,7 +50,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at ...@@ -51,7 +50,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed. which will have to be stubbed.
[Teaspoon]: https://github.com/modeset/teaspoon [Karma]: https://github.com/karma-runner/karma
[Jasmine]: https://github.com/jasmine/jasmine [Jasmine]: https://github.com/jasmine/jasmine
## RSpec ## RSpec
......
doc/user/project/img/issue_board.png

88.5 KB | W: | H:

doc/user/project/img/issue_board.png

74.7 KB | W: | H:

doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
  • 2-up
  • Swipe
  • Onion skin
# Issue board # Issue board
> [Introduced][ce-5554] in GitLab 8.11. >**Notes:**
- [Introduced][ce-5554] in GitLab 8.11.
- The Backlog column was replaced by the **Add issues** button in GitLab 8.17.
The GitLab Issue Board is a software project management tool used to plan, The GitLab Issue Board is a software project management tool used to plan,
organize, and visualize a workflow for a feature or product release. organize, and visualize a workflow for a feature or product release.
...@@ -28,13 +30,11 @@ Below is a table of the definitions used for GitLab's Issue Board. ...@@ -28,13 +30,11 @@ Below is a table of the definitions used for GitLab's Issue Board.
| **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. | | **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. |
| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. Issues inside lists are [ordered by priority](labels.md#prioritize-labels). | | **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. Issues inside lists are [ordered by priority](labels.md#prioritize-labels). |
There are three types of lists, the ones you create based on your labels, and There are two types of lists, the ones you create based on your labels, and
two default: one default:
- **Backlog** (default): shows all issues that do not fall in one of the other lists. Always appears on the very left.
- **Done** (default): shows all closed issues. Always appears on the very right.
Label list: a list based on a label. It shows all issues with that label.
- Label list: a list based on a label. It shows all opened issues with that label. - Label list: a list based on a label. It shows all opened issues with that label.
- **Done** (default): shows all closed issues. Always appears on the very right.
![GitLab Issue Board](img/issue_board.png) ![GitLab Issue Board](img/issue_board.png)
...@@ -55,10 +55,10 @@ In short, here's a list of actions you can take in an Issue Board: ...@@ -55,10 +55,10 @@ In short, here's a list of actions you can take in an Issue Board:
If you are not able to perform one or more of the things above, make sure you If you are not able to perform one or more of the things above, make sure you
have the right [permissions](#permissions). have the right [permissions](#permissions).
## First time using the Issue Board ## First time using the issue board
The first time you navigate to your Issue Board, you will be presented with the The first time you navigate to your Issue Board, you will be presented with
two default lists (**Backlog** and **Done**) and a welcoming message that gives a default list (**Done**) and a welcoming message that gives
you two options. You can either create a predefined set of labels and create you two options. You can either create a predefined set of labels and create
their corresponding lists to the Issue Board or opt-out and use your own lists. their corresponding lists to the Issue Board or opt-out and use your own lists.
...@@ -93,23 +93,26 @@ in the list's heading. A confirmation dialog will appear for you to confirm. ...@@ -93,23 +93,26 @@ in the list's heading. A confirmation dialog will appear for you to confirm.
Deleting a list doesn't have any effect in issues and labels, it's just the Deleting a list doesn't have any effect in issues and labels, it's just the
list view that is removed. You can always add it back later if you need. list view that is removed. You can always add it back later if you need.
## Searching issues in the Backlog list ## Adding issues to a list
You can add issues to a list by clicking the **Add issues** button that is
present in the upper right corner of the issue board. This will open up a modal
window where you can see all the issues that do not belong to any list.
Select one or more issues by clicking on the cards and then click **Add issues**
to add them to the selected list. You can limit the issues you want to add to
the list by filtering by author, assignee, milestone and label.
The very first time you start using the Issue Board, it is very likely your ![Bulk adding issues to lists](img/issue_boards_add_issues_modal.png)
issue tracker is already populated with labels and issues. In that case,
**Backlog** will have all the issues that don't belong to another list, and
**Done** will have all the closed ones.
For performance and visibility reasons, each list shows the first 20 issues ## Removing an issue from a list
by default. If you have more than 20, you have to start scrolling down for the
next 20 issues to appear. This can be cumbersome if your issue tracker hosts
hundreds of issues, and for that reason it is easier to search for issues to
move from **Backlog** to another list.
Start typing in the search bar under the **Backlog** list and the relevant Removing an issue from a list can be done by clicking on the issue card and then
issues will appear. clicking the **Remove from board** button in the sidebar. Under the hood, the
respective label is removed, and as such it's also removed from the list and the
board itself.
![Issue Board search Backlog](img/issue_board_search_backlog.png) ![Remove issue from list](img/issue_boards_remove_issue.png)
## Filtering issues ## Filtering issues
...@@ -142,8 +145,8 @@ A typical workflow of using the Issue Board would be: ...@@ -142,8 +145,8 @@ A typical workflow of using the Issue Board would be:
and gets automatically closed. and gets automatically closed.
For instance you can create a list based on the label of 'Frontend' and one for For instance you can create a list based on the label of 'Frontend' and one for
'Backend'. A designer can start working on an issue by dragging it from 'Backend'. A designer can start working on an issue by adding it to the
**Backlog** to 'Frontend'. That way, everyone knows that this issue is now being 'Frontend' list. That way, everyone knows that this issue is now being
worked on by the designers. Then, once they're done, all they have to do is worked on by the designers. Then, once they're done, all they have to do is
drag it over to the next list, 'Backend', where a backend developer can drag it over to the next list, 'Backend', where a backend developer can
eventually pick it up. Once they’re done, they move it to **Done**, to close the eventually pick it up. Once they’re done, they move it to **Done**, to close the
......
# Merge Requests This document was moved to [merge_requests/index.md](merge_requests/index.md).
Merge requests allow you to exchange changes you made to source code and
collaborate with other people on the same project.
## Authorization for merge requests
There are two main ways to have a merge request flow with GitLab:
1. Working with [protected branches][] in a single repository
1. Working with forks of an authoritative project
[Learn more about the authorization for merge requests.](merge_requests/authorization_for_merge_requests.md)
## Cherry-pick changes
Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
in a merged merge requests or a commit.
[Learn more about cherry-picking changes.](merge_requests/cherry_pick_changes.md)
## Merge when pipeline succeeds
When reviewing a merge request that looks ready to merge but still has one or
more CI builds running, you can set it to be merged automatically when CI
pipeline succeeds. This way, you don't have to wait for the pipeline to finish
and remember to merge the request manually.
[Learn more about merging when pipeline succeeds.](merge_requests/merge_when_pipeline_succeeds.md)
## Resolve discussion comments in merge requests reviews
Keep track of the progress during a code review with resolving comments.
Resolving comments prevents you from forgetting to address feedback and lets
you hide discussions that are no longer relevant.
[Read more about resolving discussion comments in merge requests reviews.](merge_requests/merge_request_discussion_resolution.md)
## Squash and merge
GitLab allows you to squash all changes present in a merge request into a single
commit when merging, to allow for a neater commit history.
[Learn more about squash and merge.](merge_requests/squash_and_merge)
## Resolve conflicts
When a merge request has conflicts, GitLab may provide the option to resolve
those conflicts in the GitLab UI.
[Learn more about resolving merge conflicts in the UI.](merge_requests/resolve_conflicts.md)
## Revert changes
GitLab implements Git's powerful feature to revert any commit with introducing
a **Revert** button in merge requests and commit details.
[Learn more about reverting changes in the UI](merge_requests/revert_changes.md)
## Merge requests versions
Every time you push to a branch that is tied to a merge request, a new version
of merge request diff is created. When you visit a merge request that contains
more than one pushes, you can select and compare the versions of those merge
request diffs.
[Read more about the merge requests versions.](merge_requests/versions.md)
## Work In Progress merge requests
To prevent merge requests from accidentally being accepted before they're
completely ready, GitLab blocks the "Accept" button for merge requests that
have been marked as a **Work In Progress**.
[Learn more about settings a merge request as "Work In Progress".](merge_requests/work_in_progress_merge_requests.md)
## Merge request approvals
> Included in [GitLab Enterprise Edition Starter][products].
If you want to make sure every merge request is approved by one or more people,
you can enforce this workflow by using merge request approvals. Merge request
approvals allow you to set the number of necessary approvals and predefine a
list of approvers that will need to approve every merge request in a project.
[Read more about merge request approvals.](merge_requests/merge_request_approvals.md)
## Fast-forward merge requests
> Included in [GitLab Enterprise Edition Starter][products].
If you prefer a linear Git history and a way to accept merge requests without
creating merge commits, you can configure this on a per-project basis.
[Read more about fast-forward merge requests.](merge_requests/fast_forward_merge.md)
## Ignore whitespace changes in Merge Request diff view
If you click the **Hide whitespace changes** button, you can see the diff
without whitespace changes (if there are any). This is also working when on a
specific commit page.
![MR diff](merge_requests/img/merge_request_diff.png)
>**Tip:**
You can append `?w=1` while on the diffs page of a merge request to ignore any
whitespace changes.
## Tips
Here are some tips that will help you be more efficient with merge requests in
the command line.
> **Note:**
This section might move in its own document in the future.
### Checkout merge requests locally
A merge request contains all the history from a repository, plus the additional
commits added to the branch associated with the merge request. Here's a few
tricks to checkout a merge request locally.
Please note that you can checkout a merge request locally even if the source
project is a fork (even a private fork) of the target project.
#### Checkout locally by adding a git alias
Add the following alias to your `~/.gitconfig`:
```
[alias]
mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
```
Now you can check out a particular merge request from any repository and any
remote. For example, to check out the merge request with ID 5 as shown in GitLab
from the `upstream` remote, do:
```
git mr upstream 5
```
This will fetch the merge request into a local `mr-upstream-5` branch and check
it out.
#### Checkout locally by modifying `.git/config` for a given repository
Locate the section for your GitLab remote in the `.git/config` file. It looks
like this:
```
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-ce.git
fetch = +refs/heads/*:refs/remotes/origin/*
```
You can open the file with:
```
git config -e
```
Now add the following line to the above section:
```
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
In the end, it should look like this:
```
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-ce.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
Now you can fetch all the merge requests:
```
git fetch origin
...
From https://gitlab.com/gitlab-org/gitlab-ce.git
* [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
* [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
...
```
And to check out a particular merge request:
```
git checkout origin/merge-requests/1
```
[products]: https://about.gitlab.com/products/ "GitLab products page"
[protected branches]: protected_branches.md
# Merge requests
Merge requests allow you to exchange changes you made to source code and
collaborate with other people on the same project.
## Authorization for merge requests
There are two main ways to have a merge request flow with GitLab:
1. Working with [protected branches][] in a single repository
1. Working with forks of an authoritative project
[Learn more about the authorization for merge requests.](authorization_for_merge_requests.md)
## Cherry-pick changes
Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
in a merged merge requests or a commit.
[Learn more about cherry-picking changes.](cherry_pick_changes.md)
## Merge when pipeline succeeds
When reviewing a merge request that looks ready to merge but still has one or
more CI builds running, you can set it to be merged automatically when CI
pipeline succeeds. This way, you don't have to wait for the pipeline to finish
and remember to merge the request manually.
[Learn more about merging when pipeline succeeds.](merge_when_pipeline_succeeds.md)
## Resolve discussion comments in merge requests reviews
Keep track of the progress during a code review with resolving comments.
Resolving comments prevents you from forgetting to address feedback and lets
you hide discussions that are no longer relevant.
[Read more about resolving discussion comments in merge requests reviews.](merge_request_discussion_resolution.md)
## Squash and merge
GitLab allows you to squash all changes present in a merge request into a single
commit when merging, to allow for a neater commit history.
[Learn more about squash and merge.](merge_requests/squash_and_merge)
## Resolve conflicts
When a merge request has conflicts, GitLab may provide the option to resolve
those conflicts in the GitLab UI.
[Learn more about resolving merge conflicts in the UI.](resolve_conflicts.md)
## Revert changes
GitLab implements Git's powerful feature to revert any commit with introducing
a **Revert** button in merge requests and commit details.
[Learn more about reverting changes in the UI](revert_changes.md)
## Merge requests versions
Every time you push to a branch that is tied to a merge request, a new version
of merge request diff is created. When you visit a merge request that contains
more than one pushes, you can select and compare the versions of those merge
request diffs.
[Read more about the merge requests versions.](versions.md)
## Work In Progress merge requests
To prevent merge requests from accidentally being accepted before they're
completely ready, GitLab blocks the "Accept" button for merge requests that
have been marked as a **Work In Progress**.
[Learn more about settings a merge request as "Work In Progress".](work_in_progress_merge_requests.md)
## Merge request approvals
> Included in [GitLab Enterprise Edition Starter][products].
If you want to make sure every merge request is approved by one or more people,
you can enforce this workflow by using merge request approvals. Merge request
approvals allow you to set the number of necessary approvals and predefine a
list of approvers that will need to approve every merge request in a project.
[Read more about merge request approvals.](merge_requests/merge_request_approvals.md)
## Fast-forward merge requests
> Included in [GitLab Enterprise Edition Starter][products].
If you prefer a linear Git history and a way to accept merge requests without
creating merge commits, you can configure this on a per-project basis.
[Read more about fast-forward merge requests.](merge_requests/fast_forward_merge.md)
## Ignore whitespace changes in Merge Request diff view
If you click the **Hide whitespace changes** button, you can see the diff
without whitespace changes (if there are any). This is also working when on a
specific commit page.
![MR diff](img/merge_request_diff.png)
>**Tip:**
You can append `?w=1` while on the diffs page of a merge request to ignore any
whitespace changes.
## Tips
Here are some tips that will help you be more efficient with merge requests in
the command line.
> **Note:**
This section might move in its own document in the future.
### Checkout merge requests locally
A merge request contains all the history from a repository, plus the additional
commits added to the branch associated with the merge request. Here's a few
tricks to checkout a merge request locally.
Please note that you can checkout a merge request locally even if the source
project is a fork (even a private fork) of the target project.
#### Checkout locally by adding a git alias
Add the following alias to your `~/.gitconfig`:
```
[alias]
mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
```
Now you can check out a particular merge request from any repository and any
remote. For example, to check out the merge request with ID 5 as shown in GitLab
from the `upstream` remote, do:
```
git mr upstream 5
```
This will fetch the merge request into a local `mr-upstream-5` branch and check
it out.
#### Checkout locally by modifying `.git/config` for a given repository
Locate the section for your GitLab remote in the `.git/config` file. It looks
like this:
```
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-ce.git
fetch = +refs/heads/*:refs/remotes/origin/*
```
You can open the file with:
```
git config -e
```
Now add the following line to the above section:
```
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
In the end, it should look like this:
```
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-ce.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
Now you can fetch all the merge requests:
```
git fetch origin
...
From https://gitlab.com/gitlab-org/gitlab-ce.git
* [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
* [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
...
```
And to check out a particular merge request:
```
git checkout origin/merge-requests/1
```
[products]: https://about.gitlab.com/products/ "GitLab products page"
[protected branches]: protected_branches.md
# Merge requests versions # Merge requests versions
> Will be [introduced][ce-5467] in GitLab 8.12. >**Notes:**
- [Introduced][ce-5467] in GitLab 8.12.
- Comments are disabled while viewing outdated merge versions or comparing to
versions other than base.
- Merge request versions are based on push not on commit. So, if you pushed 5
commits in a single push, it will be a single option in the dropdown. If you
pushed 5 times, that will count for 5 options.
Every time you push to a branch that is tied to a merge request, a new version Every time you push to a branch that is tied to a merge request, a new version
of merge request diff is created. When you visit a merge request that contains of merge request diff is created. When you visit a merge request that contains
...@@ -30,13 +36,4 @@ changes appears as a system note. ...@@ -30,13 +36,4 @@ changes appears as a system note.
![Merge request versions system note](img/versions_system_note.png) ![Merge request versions system note](img/versions_system_note.png)
---
>**Notes:**
- Comments are disabled while viewing outdated merge versions or comparing to
versions other than base.
- Merge request versions are based on push not on commit. So, if you pushed 5
commits in a single push, it will be a single option in the dropdown. If you
pushed 5 times, that will count for 5 options.
[ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467 [ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
- [Web Editor](../user/project/repository/web_editor.md) - [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md) - [Releases](releases.md)
- [Milestones](milestones.md) - [Milestones](milestones.md)
- [Merge Requests](../user/project/merge_requests.md) - [Merge Requests](../user/project/merge_requests/index.md)
- [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md) - [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md)
- [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md) - [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md)
- [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md) - [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md)
......
...@@ -84,7 +84,7 @@ module API ...@@ -84,7 +84,7 @@ module API
branch = user_project.repository.find_branch(params[:branch]) branch = user_project.repository.find_branch(params[:branch])
not_found!("Branch") unless branch not_found!("Branch") unless branch
protected_branch = user_project.protected_branches.find_by(name: branch.name) protected_branch = user_project.protected_branches.find_by(name: branch.name)
protected_branch.destroy if protected_branch protected_branch&.destroy
present branch, with: Entities::RepoBranch, project: user_project present branch, with: Entities::RepoBranch, project: user_project
end end
......
...@@ -429,9 +429,7 @@ module API ...@@ -429,9 +429,7 @@ module API
expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author } expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author }
expose :author_username do |event, options| expose :author_username do |event, options|
if event.author event.author&.username
event.author.username
end
end end
end end
......
...@@ -117,7 +117,7 @@ module API ...@@ -117,7 +117,7 @@ module API
authorize! :push_code, user_project authorize! :push_code, user_project
file_params = declared_params(include_missing: false) file_params = declared_params(include_missing: false)
result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute
if result[:status] == :success if result[:status] == :success
status(200) status(200)
......
...@@ -84,7 +84,9 @@ module API ...@@ -84,7 +84,9 @@ module API
end end
params do params do
requires :email, type: String, desc: 'The email of the user' requires :email, type: String, desc: 'The email of the user'
requires :password, type: String, desc: 'The password of the new user' optional :password, type: String, desc: 'The password of the new user'
optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token'
at_least_one_of :password, :reset_password
requires :name, type: String, desc: 'The name of the user' requires :name, type: String, desc: 'The name of the user'
requires :username, type: String, desc: 'The username of the user' requires :username, type: String, desc: 'The username of the user'
use :optional_attributes use :optional_attributes
...@@ -96,8 +98,18 @@ module API ...@@ -96,8 +98,18 @@ module API
user_params = declared_params(include_missing: false) user_params = declared_params(include_missing: false)
identity_attrs = user_params.slice(:provider, :extern_uid) identity_attrs = user_params.slice(:provider, :extern_uid)
confirm = user_params.delete(:confirm) confirm = user_params.delete(:confirm)
user = User.new(user_params.except(:extern_uid, :provider, :reset_password))
if user_params.delete(:reset_password)
user.attributes = {
force_random_password: true,
password_expires_at: nil,
created_by_id: current_user.id
}
user.generate_password
user.generate_reset_token
end
user = User.new(user_params.except(:extern_uid, :provider))
user.skip_confirmation! unless confirm user.skip_confirmation! unless confirm
if identity_attrs.any? if identity_attrs.any?
......
...@@ -58,7 +58,7 @@ module Gitlab ...@@ -58,7 +58,7 @@ module Gitlab
def helpers(*nodes) def helpers(*nodes)
nodes.each do |symbol| nodes.each do |symbol|
define_method("#{symbol}_defined?") do define_method("#{symbol}_defined?") do
@entries[symbol].specified? if @entries[symbol] @entries[symbol]&.specified?
end end
define_method("#{symbol}_value") do define_method("#{symbol}_value") do
......
...@@ -26,11 +26,59 @@ module Gitlab ...@@ -26,11 +26,59 @@ module Gitlab
add_index(table_name, column_name, options) add_index(table_name, column_name, options)
end end
# Adds a foreign key with only minimal locking on the tables involved.
#
# This method only requires minimal locking when using PostgreSQL. When
# using MySQL this method will use Rails' default `add_foreign_key`.
#
# source - The source table containing the foreign key.
# target - The target table the key points to.
# column - The name of the column to create the foreign key on.
# on_delete - The action to perform when associated data is removed,
# defaults to "CASCADE".
def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade)
# Transactions would result in ALTER TABLE locks being held for the
# duration of the transaction, defeating the purpose of this method.
if transaction_open?
raise 'add_concurrent_foreign_key can not be run inside a transaction'
end
# While MySQL does allow disabling of foreign keys it has no equivalent
# of PostgreSQL's "VALIDATE CONSTRAINT". As a result we'll just fall
# back to the normal foreign key procedure.
if Database.mysql?
return add_foreign_key(source, target,
column: column,
on_delete: on_delete)
end
disable_statement_timeout
key_name = "fk_#{source}_#{target}_#{column}"
# Using NOT VALID allows us to create a key without immediately
# validating it. This means we keep the ALTER TABLE lock only for a
# short period of time. The key _is_ enforced for any newly created
# data.
execute <<-EOF.strip_heredoc
ALTER TABLE #{source}
ADD CONSTRAINT #{key_name}
FOREIGN KEY (#{column})
REFERENCES #{target} (id)
ON DELETE #{on_delete} NOT VALID;
EOF
# Validate the existing constraint. This can potentially take a very
# long time to complete, but fortunately does not lock the source table
# while running.
execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};")
end
# Long-running migrations may take more than the timeout allowed by # Long-running migrations may take more than the timeout allowed by
# the database. Disable the session's statement timeout to ensure # the database. Disable the session's statement timeout to ensure
# migrations don't get killed prematurely. (PostgreSQL only) # migrations don't get killed prematurely. (PostgreSQL only)
def disable_statement_timeout def disable_statement_timeout
ActiveRecord::Base.connection.execute('SET statement_timeout TO 0') if Database.postgresql? execute('SET statement_timeout TO 0') if Database.postgresql?
end end
# Updates the value of a column in batches. # Updates the value of a column in batches.
......
...@@ -46,7 +46,7 @@ module Gitlab ...@@ -46,7 +46,7 @@ module Gitlab
end end
def diffs_count def diffs_count
diffs.size if diffs diffs&.size
end end
def compare def compare
...@@ -58,7 +58,7 @@ module Gitlab ...@@ -58,7 +58,7 @@ module Gitlab
end end
def compare_timeout def compare_timeout
diffs.overflow? if diffs diffs&.overflow?
end end
def reverse_compare? def reverse_compare?
......
...@@ -13,7 +13,7 @@ module Gitlab ...@@ -13,7 +13,7 @@ module Gitlab
end end
def data def data
lines.join("\n") if lines lines&.join("\n")
end end
def name def name
......
...@@ -112,7 +112,7 @@ module Gitlab ...@@ -112,7 +112,7 @@ module Gitlab
def self.tag_transaction(name, value) def self.tag_transaction(name, value)
trans = current_transaction trans = current_transaction
trans.add_tag(name, value) if trans trans&.add_tag(name, value)
end end
# Sets the action of the current transaction (if any) # Sets the action of the current transaction (if any)
...@@ -121,7 +121,7 @@ module Gitlab ...@@ -121,7 +121,7 @@ module Gitlab
def self.action=(action) def self.action=(action)
trans = current_transaction trans = current_transaction
trans.action = action if trans trans&.action = action
end end
# Tracks an event. # Tracks an event.
...@@ -130,7 +130,7 @@ module Gitlab ...@@ -130,7 +130,7 @@ module Gitlab
def self.add_event(*args) def self.add_event(*args)
trans = current_transaction trans = current_transaction
trans.add_event(*args) if trans trans&.add_event(*args)
end end
# Returns the prefix to use for the name of a series. # Returns the prefix to use for the name of a series.
......
require_relative '../../migration_helpers'
module RuboCop
module Cop
module Migration
# Cop that checks if `add_concurrent_foreign_key` is used instead of
# `add_foreign_key`.
class AddConcurrentForeignKey < RuboCop::Cop::Cop
include MigrationHelpers
MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'
def on_send(node)
return unless in_migration?(node)
name = node.children[1]
add_offense(node, :selector) if name == :add_foreign_key
end
def method_name(node)
node.children.first
end
end
end
end
end
require_relative 'cop/gem_fetcher' require_relative 'cop/gem_fetcher'
require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_column'
require_relative 'cop/migration/add_column_with_default' require_relative 'cop/migration/add_column_with_default'
require_relative 'cop/migration/add_concurrent_foreign_key'
require_relative 'cop/migration/add_index' require_relative 'cop/migration/add_index'
require 'spec_helper'
describe DashboardController do
let(:user) { create(:user) }
let(:project) { create(:project) }
before do
project.team << [user, :master]
sign_in(user)
end
describe 'GET issues' do
it_behaves_like 'issuables list meta-data', :issue, :issues
end
describe 'GET merge requests' do
it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests
end
end
...@@ -24,6 +24,8 @@ describe Projects::IssuesController do ...@@ -24,6 +24,8 @@ describe Projects::IssuesController do
project.team << [user, :developer] project.team << [user, :developer]
end end
it_behaves_like "issuables list meta-data", :issue
it "returns index" do it "returns index" do
get :index, namespace_id: project.namespace.path, project_id: project.path get :index, namespace_id: project.namespace.path, project_id: project.path
......
...@@ -310,6 +310,8 @@ describe Projects::MergeRequestsController do ...@@ -310,6 +310,8 @@ describe Projects::MergeRequestsController do
end end
describe 'GET index' do describe 'GET index' do
let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
def get_merge_requests(page = nil) def get_merge_requests(page = nil)
get :index, get :index,
namespace_id: project.namespace.to_param, namespace_id: project.namespace.to_param,
...@@ -317,6 +319,8 @@ describe Projects::MergeRequestsController do ...@@ -317,6 +319,8 @@ describe Projects::MergeRequestsController do
state: 'opened', page: page.to_param state: 'opened', page: page.to_param
end end
it_behaves_like "issuables list meta-data", :merge_request
context 'when page param' do context 'when page param' do
let(:last_page) { project.merge_requests.page().total_pages } let(:last_page) { project.merge_requests.page().total_pages }
let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
......
...@@ -54,7 +54,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -54,7 +54,7 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.issue-boards-sidebar') expect(page).to have_selector('.issue-boards-sidebar')
find('.gutter-toggle').click find('.gutter-toggle').trigger('click')
expect(page).not_to have_selector('.issue-boards-sidebar') expect(page).not_to have_selector('.issue-boards-sidebar')
end end
......
...@@ -101,6 +101,22 @@ feature 'Environment', :feature do ...@@ -101,6 +101,22 @@ feature 'Environment', :feature do
scenario 'it shows the terminal button' do scenario 'it shows the terminal button' do
expect(page).to have_terminal_button expect(page).to have_terminal_button
end end
context 'web terminal', :js do
before do
# Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
allow_any_instance_of(Environment).to receive(:terminals) { nil }
visit terminal_namespace_project_environment_path(project.namespace, project, environment)
end
it 'displays a web terminal' do
expect(page).to have_selector('#terminal')
end
it 'displays a link to the environment external url' do
expect(page).to have_link(nil, href: environment.external_url)
end
end
end end
context 'for developer' do context 'for developer' do
......
require 'spec_helper'
feature 'Groups members list', feature: true do
let(:user1) { create(:user, name: 'John Doe') }
let(:user2) { create(:user, name: 'Mary Jane') }
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
background do
login_as(user1)
end
scenario 'show members from current group and parent' do
group.add_developer(user1)
nested_group.add_developer(user2)
visit group_group_members_path(nested_group)
expect(first_row.text).to include(user1.name)
expect(second_row.text).to include(user2.name)
end
scenario 'show user once if member of both current group and parent' do
group.add_developer(user1)
nested_group.add_developer(user1)
visit group_group_members_path(nested_group)
expect(first_row.text).to include(user1.name)
expect(second_row).to be_blank
end
def first_row
page.all('ul.content-list > li')[0]
end
def second_row
page.all('ul.content-list > li')[1]
end
end
require 'rails_helper'
describe 'issuable list', feature: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
issuable_types = [:issue, :merge_request]
before do
project.add_user(user, :developer)
login_as(user)
issuable_types.each { |type| create_issuables(type) }
end
issuable_types.each do |issuable_type|
it "avoids N+1 database queries for #{issuable_type.to_s.humanize.pluralize}" do
control_count = ActiveRecord::QueryRecorder.new { visit_issuable_list(issuable_type) }.count
create_issuables(issuable_type)
expect { visit_issuable_list(issuable_type) }.not_to exceed_query_limit(control_count)
end
it "counts upvotes, downvotes and notes count for each #{issuable_type.to_s.humanize}" do
visit_issuable_list(issuable_type)
expect(first('.fa-thumbs-up').find(:xpath, '..')).to have_content(1)
expect(first('.fa-thumbs-down').find(:xpath, '..')).to have_content(1)
expect(first('.fa-comments').find(:xpath, '..')).to have_content(2)
end
end
def visit_issuable_list(issuable_type)
if issuable_type == :issue
visit namespace_project_issues_path(project.namespace, project)
else
visit namespace_project_merge_requests_path(project.namespace, project)
end
end
def create_issuables(issuable_type)
3.times do
if issuable_type == :issue
issuable = create(:issue, project: project, author: user)
else
issuable = create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
end
2.times do
create(:note_on_issue, noteable: issuable, project: project, note: 'Test note')
end
create(:award_emoji, :downvote, awardable: issuable)
create(:award_emoji, :upvote, awardable: issuable)
end
end
end
...@@ -141,7 +141,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do ...@@ -141,7 +141,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do
click_on 'Changes' click_on 'Changes'
wait_for_ajax wait_for_ajax
find('.click-to-expand').click click_link 'Expand all'
wait_for_ajax wait_for_ajax
expect(page).to have_content('Gregor Samsa woke from troubled dreams') expect(page).to have_content('Gregor Samsa woke from troubled dreams')
......
...@@ -66,7 +66,7 @@ feature 'Mini Pipeline Graph', :js, :feature do ...@@ -66,7 +66,7 @@ feature 'Mini Pipeline Graph', :js, :feature do
end end
it 'should close when toggle is clicked again' do it 'should close when toggle is clicked again' do
toggle.click toggle.trigger('click')
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end end
......
require 'spec_helper'
describe GroupMembersFinder, '#execute' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, :access_requestable, parent: group) }
let(:user1) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:user4) { create(:user) }
it 'returns members for top-level group' do
member1 = group.add_master(user1)
member2 = group.add_master(user2)
member3 = group.add_master(user3)
result = described_class.new(group).execute
expect(result.to_a).to eq([member3, member2, member1])
end
it 'returns members for nested group' do
group.add_master(user2)
nested_group.request_access(user4)
member1 = group.add_master(user1)
member3 = nested_group.add_master(user2)
member4 = nested_group.add_master(user3)
result = described_class.new(nested_group).execute
expect(result.to_a).to eq([member4, member3, member1])
end
end
...@@ -40,7 +40,7 @@ require('~/behaviors/quick_submit'); ...@@ -40,7 +40,7 @@ require('~/behaviors/quick_submit');
expect($('input[type=submit]')).toBeDisabled(); expect($('input[type=submit]')).toBeDisabled();
return expect($('button[type=submit]')).toBeDisabled(); return expect($('button[type=submit]')).toBeDisabled();
}); });
// We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll
// only run the tests that apply to the current platform // only run the tests that apply to the current platform
if (navigator.userAgent.match(/Macintosh/)) { if (navigator.userAgent.match(/Macintosh/)) {
it('responds to Meta+Enter', function() { it('responds to Meta+Enter', function() {
......
%header.navbar.navbar-fixed-top.navbar-gitlab.nav_header_class %header.navbar.navbar-gitlab.nav_header_class
.container-fluid .container-fluid
.header-content .header-content
%button.side-nav-toggle %button.side-nav-toggle
......
...@@ -12,15 +12,14 @@ describe Gitlab::Database::MigrationHelpers, lib: true do ...@@ -12,15 +12,14 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
describe '#add_concurrent_index' do describe '#add_concurrent_index' do
context 'outside a transaction' do context 'outside a transaction' do
before do before do
expect(model).to receive(:transaction_open?).and_return(false) allow(model).to receive(:transaction_open?).and_return(false)
unless Gitlab::Database.postgresql?
allow_any_instance_of(Gitlab::Database::MigrationHelpers).to receive(:disable_statement_timeout)
end
end end
context 'using PostgreSQL' do context 'using PostgreSQL' do
before { expect(Gitlab::Database).to receive(:postgresql?).and_return(true) } before do
allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
allow(model).to receive(:disable_statement_timeout)
end
it 'creates the index concurrently' do it 'creates the index concurrently' do
expect(model).to receive(:add_index). expect(model).to receive(:add_index).
...@@ -59,6 +58,71 @@ describe Gitlab::Database::MigrationHelpers, lib: true do ...@@ -59,6 +58,71 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end end
end end
describe '#add_concurrent_foreign_key' do
context 'inside a transaction' do
it 'raises an error' do
expect(model).to receive(:transaction_open?).and_return(true)
expect do
model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
end.to raise_error(RuntimeError)
end
end
context 'outside a transaction' do
before do
allow(model).to receive(:transaction_open?).and_return(false)
end
context 'using MySQL' do
it 'creates a regular foreign key' do
allow(Gitlab::Database).to receive(:mysql?).and_return(true)
expect(model).to receive(:add_foreign_key).
with(:projects, :users, column: :user_id, on_delete: :cascade)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
end
end
context 'using PostgreSQL' do
before do
allow(Gitlab::Database).to receive(:mysql?).and_return(false)
end
it 'creates a concurrent foreign key' do
expect(model).to receive(:disable_statement_timeout)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
end
end
end
end
describe '#disable_statement_timeout' do
context 'using PostgreSQL' do
it 'disables statement timeouts' do
expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
expect(model).to receive(:execute).with('SET statement_timeout TO 0')
model.disable_statement_timeout
end
end
context 'using MySQL' do
it 'does nothing' do
expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
expect(model).not_to receive(:execute)
model.disable_statement_timeout
end
end
end
describe '#update_column_in_batches' do describe '#update_column_in_batches' do
before do before do
create_list(:empty_project, 5) create_list(:empty_project, 5)
......
...@@ -122,7 +122,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do ...@@ -122,7 +122,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
end end
def delete_file(branch_name, file_name) def delete_file(branch_name, file_name)
Files::DeleteService.new( Files::DestroyService.new(
project, project,
current_user, current_user,
start_branch: branch_name, start_branch: branch_name,
......
...@@ -129,6 +129,14 @@ describe Member, models: true do ...@@ -129,6 +129,14 @@ describe Member, models: true do
it { expect(described_class.request).not_to include @accepted_request_member } it { expect(described_class.request).not_to include @accepted_request_member }
end end
describe '.non_request' do
it { expect(described_class.non_request).to include @master }
it { expect(described_class.non_request).to include @invited_member }
it { expect(described_class.non_request).to include @accepted_invite_member }
it { expect(described_class.non_request).not_to include @requested_member }
it { expect(described_class.non_request).to include @accepted_request_member }
end
describe '.developers' do describe '.developers' do
subject { described_class.developers.to_a } subject { described_class.developers.to_a }
......
...@@ -202,6 +202,18 @@ describe API::Users, api: true do ...@@ -202,6 +202,18 @@ describe API::Users, api: true do
expect(new_user.external).to be_truthy expect(new_user.external).to be_truthy
end end
it "creates user with reset password" do
post api('/users', admin), attributes_for(:user, reset_password: true).except(:password)
expect(response).to have_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
expect(new_user).not_to eq(nil)
expect(new_user.recently_sent_password_reset?).to eq(true)
end
it "does not create user with invalid email" do it "does not create user with invalid email" do
post api('/users', admin), post api('/users', admin),
email: 'invalid email', email: 'invalid email',
......
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/migration/add_concurrent_foreign_key'
describe RuboCop::Cop::Migration::AddConcurrentForeignKey do
include CopHelper
let(:cop) { described_class.new }
context 'outside of a migration' do
it 'does not register any offenses' do
inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end')
expect(cop.offenses).to be_empty
end
end
context 'in a migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
end
it 'registers an offense when using add_foreign_key' do
inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
expect(cop.offenses.map(&:line)).to eq([1])
end
end
end
end
...@@ -120,11 +120,20 @@ describe Issues::BuildService, services: true do ...@@ -120,11 +120,20 @@ describe Issues::BuildService, services: true do
end end
describe '#execute' do describe '#execute' do
let(:milestone) { create(:milestone, project: project) }
it 'builds a new issues with given params' do it 'builds a new issues with given params' do
issue = described_class.new(project, user, title: 'Issue #1', description: 'Issue description').execute issue = described_class.new(
project,
user,
title: 'Issue #1',
description: 'Issue description',
milestone_id: milestone.id,
).execute
expect(issue.title).to eq('Issue #1') expect(issue.title).to eq('Issue #1')
expect(issue.description).to eq('Issue description') expect(issue.description).to eq('Issue description')
expect(issue.milestone).to eq(milestone)
end end
end end
end end
...@@ -46,6 +46,7 @@ describe Issues::CreateService, services: true do ...@@ -46,6 +46,7 @@ describe Issues::CreateService, services: true do
expect(issue).to be_persisted expect(issue).to be_persisted
expect(issue.title).to eq('Awesome issue') expect(issue.title).to eq('Awesome issue')
expect(issue.description).to eq('please fix')
expect(issue.assignee).to be_nil expect(issue.assignee).to be_nil
expect(issue.labels).to be_empty expect(issue.labels).to be_empty
expect(issue.milestone).to be_nil expect(issue.milestone).to be_nil
......
shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
before do
@issuable_ids = []
2.times do
if issuable_type == :issue
issuable = create(issuable_type, project: project)
else
issuable = create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
end
@issuable_ids << issuable.id
issuable.id.times { create(:note, noteable: issuable, project: issuable.project) }
(issuable.id + 1).times { create(:award_emoji, :downvote, awardable: issuable) }
(issuable.id + 2).times { create(:award_emoji, :upvote, awardable: issuable) }
end
end
it "creates indexed meta-data object for issuable notes and votes count" do
if action
get action
else
get :index, namespace_id: project.namespace.path, project_id: project.path
end
meta_data = assigns(:issuable_meta_data)
@issuable_ids.each do |id|
expect(meta_data[id].notes_count).to eq(id)
expect(meta_data[id].downvotes).to eq(id + 1)
expect(meta_data[id].upvotes).to eq(id + 2)
end
end
end
...@@ -58,7 +58,7 @@ shared_examples 'new issuable record that supports slash commands' do ...@@ -58,7 +58,7 @@ shared_examples 'new issuable record that supports slash commands' do
let(:example_params) do let(:example_params) do
{ {
assignee: create(:user), assignee: create(:user),
milestone_id: double(:milestone), milestone_id: 1,
description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}") description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
} }
end end
......
...@@ -3,7 +3,7 @@ module SlashCommandsHelpers ...@@ -3,7 +3,7 @@ module SlashCommandsHelpers
Sidekiq::Testing.fake! do Sidekiq::Testing.fake! do
page.within('.js-main-target-form') do page.within('.js-main-target-form') do
fill_in 'note[note]', with: text fill_in 'note[note]', with: text
click_button 'Comment' find('.comment-btn').trigger('click')
end end
end end
end end
......
...@@ -77,6 +77,6 @@ end ...@@ -77,6 +77,6 @@ end
def submit_time(slash_command) def submit_time(slash_command)
fill_in 'note[note]', with: slash_command fill_in 'note[note]', with: slash_command
click_button 'Comment' find('.comment-btn').trigger('click')
wait_for_ajax wait_for_ajax
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