summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md33
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/contextual_sidebar.js8
-rw-r--r--app/assets/javascripts/fly_out_nav.js2
-rw-r--r--app/assets/javascripts/notes/components/issue_note.vue3
-rw-r--r--app/assets/stylesheets/framework/contextual-sidebar.scss38
-rw-r--r--app/assets/stylesheets/framework/variables.scss3
-rw-r--r--app/assets/stylesheets/pages/boards.scss6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss2
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/helpers/preferences_helper.rb2
-rw-r--r--app/models/issue.rb3
-rw-r--r--app/models/user.rb15
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--changelogs/unreleased/15832-fix-access-level-update-for-requesters.yml5
-rw-r--r--changelogs/unreleased/35724-animate-sidebar.yml5
-rw-r--r--changelogs/unreleased/sh-remove-allocation-tracking-influxdb.yml5
-rw-r--r--db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb1
-rw-r--r--db/migrate/20160610301627_remove_notification_level_from_users.rb1
-rw-r--r--db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb1
-rw-r--r--db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb1
-rw-r--r--db/migrate/20160729173930_remove_project_id_from_spam_logs.rb1
-rw-r--r--db/migrate/20160831223750_remove_features_enabled_from_projects.rb1
-rw-r--r--db/migrate/20160913162434_remove_projects_pushes_since_gc.rb1
-rw-r--r--db/migrate/20161018024550_remove_priority_from_labels.rb1
-rw-r--r--db/migrate/20161201160452_migrate_project_statistics.rb1
-rw-r--r--db/migrate/20170222143500_remove_old_project_id_columns.rb1
-rw-r--r--db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb1
-rw-r--r--db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb1
-rw-r--r--db/migrate/20171106154015_remove_issues_branch_name.rb1
-rw-r--r--db/post_migrate/20170523073948_remove_assignee_id_from_issue.rb48
-rw-r--r--db/schema.rb2
-rw-r--r--doc/administration/index.md2
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md3
-rw-r--r--doc/administration/operations.md8
-rw-r--r--doc/administration/operations/index.md16
-rw-r--r--doc/development/automatic_ce_ee_merge.md2
-rw-r--r--doc/operations/README.md2
-rw-r--r--features/steps/groups.rb2
-rw-r--r--lib/api/entities.rb17
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/gitlab/git/storage/checker.rb24
-rw-r--r--lib/gitlab/metrics/samplers/influx_sampler.rb24
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb27
-rw-r--r--rubocop/cop/migration/remove_column.rb30
-rw-r--r--rubocop/migration_helpers.rb6
-rw-r--r--rubocop/rubocop.rb1
-rwxr-xr-xscripts/trigger-build-omnibus2
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb19
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb20
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/features/groups/members/manage_members.rb21
-rw-r--r--spec/helpers/preferences_helper_spec.rb74
-rw-r--r--spec/javascripts/fly_out_nav_spec.js4
-rw-r--r--spec/javascripts/notes/components/issue_note_spec.js15
-rw-r--r--spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb23
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb23
-rw-r--r--spec/migrations/remove_assignee_id_from_issue_spec.rb37
-rw-r--r--spec/models/user_spec.rb35
-rw-r--r--spec/requests/api/groups_spec.rb62
-rw-r--r--spec/requests/api/issues_spec.rb14
-rw-r--r--spec/rubocop/cop/migration/remove_column_spec.rb68
-rw-r--r--spec/services/members/authorized_destroy_service_spec.rb2
-rw-r--r--spec/services/users/keys_count_service_spec.rb10
70 files changed, 607 insertions, 203 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6088a1b3515..adf097b52f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.2.4 (2017-12-07)
+
+### Security (5 changes)
+
+- Fix e-mail address disclosure through member search fields
+- Prevent creating issues through API when user does not have permissions
+- Prevent an information disclosure in the Groups API
+- Fix user without access to private Wiki being able to see it on the project page
+- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment
+
+
## 10.2.3 (2017-11-30)
### Fixed (7 changes)
@@ -237,6 +248,17 @@ entry.
- Add Gitaly metrics to the performance bar.
+## 10.1.5 (2017-12-07)
+
+### Security (5 changes)
+
+- Fix e-mail address disclosure through member search fields
+- Prevent creating issues through API when user does not have permissions
+- Prevent an information disclosure in the Groups API
+- Fix user without access to private Wiki being able to see it on the project page
+- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment
+
+
## 10.1.4 (2017-11-14)
### Fixed (4 changes)
@@ -485,6 +507,17 @@ entry.
- creation of keys moved to services. !13331 (haseebeqx)
- Add username as GL_USERNAME in hooks.
+## 10.0.7 (2017-12-07)
+
+### Security (5 changes)
+
+- Fix e-mail address disclosure through member search fields
+- Prevent creating issues through API when user does not have permissions
+- Prevent an information disclosure in the Groups API
+- Fix user without access to private Wiki being able to see it on the project page
+- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment
+
+
## 10.0.5 (2017-11-03)
- [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258
diff --git a/Gemfile b/Gemfile
index 93003ed96c4..e9701fab27a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -283,7 +283,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
- gem 'prometheus-client-mmap', '~> 0.7.0.beta39'
+ gem 'prometheus-client-mmap', '~> 0.7.0.beta43'
gem 'raindrops', '~> 0.18'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index b5ca351fea8..efae71efdb7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1113,7 +1113,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.7.0.beta39)
+ prometheus-client-mmap (~> 0.7.0.beta43)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index 46b68ebe158..cd20dde2951 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -28,7 +28,7 @@ export default class ContextualSidebar {
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false));
this.$sidebarToggle.on('click', () => {
- const value = !this.$sidebar.hasClass('sidebar-icons-only');
+ const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop');
this.toggleCollapsedSidebar(value);
});
@@ -43,16 +43,16 @@ export default class ContextualSidebar {
}
toggleSidebarNav(show) {
- this.$sidebar.toggleClass('nav-sidebar-expanded', show);
+ this.$sidebar.toggleClass('sidebar-expanded-mobile', show);
this.$overlay.toggleClass('mobile-nav-open', show);
- this.$sidebar.removeClass('sidebar-icons-only');
+ this.$sidebar.removeClass('sidebar-collapsed-desktop');
}
toggleCollapsedSidebar(collapsed) {
const breakpoint = bp.getBreakpointSize();
if (this.$sidebar.length) {
- this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
+ this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed);
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
}
ContextualSidebar.setCollapsedCookie(collapsed);
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 98837c3b2a0..6110d961609 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -21,7 +21,7 @@ let headerHeight = 50;
export const getHeaderHeight = () => headerHeight;
-export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-icons-only');
+export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop');
export const canShowActiveSubItems = (el) => {
if (el.classList.contains('active') && !isSidebarCollapsed()) {
diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue
index 8c81c5d6df3..3ceb961f58e 100644
--- a/app/assets/javascripts/notes/components/issue_note.vue
+++ b/app/assets/javascripts/notes/components/issue_note.vue
@@ -1,5 +1,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
+ import { escape } from 'underscore';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
@@ -85,7 +86,7 @@
};
this.isRequesting = true;
this.oldContent = this.note.note_html;
- this.note.note_html = noteText;
+ this.note.note_html = escape(noteText);
this.updateNote(data)
.then(() => {
diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss
index b73932eb7e1..26a2db99e0a 100644
--- a/app/assets/stylesheets/framework/contextual-sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual-sidebar.scss
@@ -1,4 +1,6 @@
.page-with-contextual-sidebar {
+ transition: padding-left $sidebar-transition-duration;
+
@media (min-width: $screen-md-min) {
padding-left: $contextual-sidebar-collapsed-width;
}
@@ -27,8 +29,10 @@
.context-header {
position: relative;
margin-right: 2px;
+ width: $contextual-sidebar-width;
a {
+ transition: padding $sidebar-transition-duration;
font-weight: $gl-font-weight-bold;
display: flex;
align-items: center;
@@ -63,10 +67,10 @@
}
.nav-sidebar {
+ transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
position: fixed;
z-index: 400;
width: $contextual-sidebar-width;
- transition: left $sidebar-transition-duration;
top: $header-height;
bottom: 0;
left: 0;
@@ -74,16 +78,15 @@
box-shadow: inset -2px 0 0 $border-color;
transform: translate3d(0, 0, 0);
- &:not(.sidebar-icons-only) {
+ &:not(.sidebar-collapsed-desktop) {
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
box-shadow: inset -2px 0 0 $border-color,
2px 1px 3px $dropdown-shadow-color;
}
}
- &.sidebar-icons-only {
- width: auto;
- min-width: $contextual-sidebar-collapsed-width;
+ &.sidebar-collapsed-desktop {
+ width: $contextual-sidebar-collapsed-width;
.nav-sidebar-inner-scroll {
overflow-x: hidden;
@@ -108,12 +111,11 @@
}
}
- &.nav-sidebar-expanded {
+ &.sidebar-expanded-mobile {
left: 0;
}
a {
- transition: none;
text-decoration: none;
}
@@ -126,9 +128,10 @@
white-space: nowrap;
a {
+ transition: padding $sidebar-transition-duration;
display: flex;
align-items: center;
- padding: 12px 16px;
+ padding: 12px 15px;
color: $gl-text-color-secondary;
}
@@ -288,7 +291,8 @@
> a {
margin-left: 4px;
- padding-left: 12px;
+ // Subtract width of left border on active element
+ padding-left: 11px;
}
.badge {
@@ -313,6 +317,7 @@
.toggle-sidebar-button,
.close-nav-button {
width: $contextual-sidebar-width - 2px;
+ transition: width $sidebar-transition-duration;
position: fixed;
bottom: 0;
padding: 16px;
@@ -343,20 +348,21 @@
}
}
+.collapse-text {
+ white-space: nowrap;
+ overflow: hidden;
+}
-.sidebar-icons-only {
+.sidebar-collapsed-desktop {
.context-header {
- height: 61px;
+ height: 60px;
+ width: $contextual-sidebar-collapsed-width;
a {
padding: 10px 4px;
}
}
- li a {
- padding: 12px 15px;
- }
-
.sidebar-top-level-items > li {
&.active a {
padding-left: 12px;
@@ -374,8 +380,8 @@
}
.toggle-sidebar-button {
- width: $contextual-sidebar-collapsed-width - 2px;
padding: 16px;
+ width: $contextual-sidebar-collapsed-width - 2px;
.collapse-text,
.icon-angle-double-left {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 4f99c27eff1..a6fa96d834c 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -5,10 +5,9 @@ $grid-size: 8px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
-$sidebar-transition-duration: .15s;
+$sidebar-transition-duration: .3s;
$sidebar-breakpoint: 1024px;
$default-transition-duration: .15s;
-$right-sidebar-transition-duration: .3s;
$contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 3683afa07de..862ea379cbc 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -57,7 +57,7 @@
position: relative;
@media (min-width: $screen-sm-min) {
- transition: width $right-sidebar-transition-duration;
+ transition: width $sidebar-transition-duration;
width: 100%;
&.is-compact {
@@ -453,8 +453,8 @@
.right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active {
- transition: width $right-sidebar-transition-duration,
- padding $right-sidebar-transition-duration;
+ transition: width $sidebar-transition-duration,
+ padding $sidebar-transition-duration;
}
&.boards-sidebar-slide-enter,
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 11ee1232bfe..32f2fa88236 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -126,7 +126,7 @@
top: $header-height;
bottom: 0;
right: 0;
- transition: width $right-sidebar-transition-duration;
+ transition: width $sidebar-transition-duration;
background: $gray-light;
z-index: 200;
overflow: hidden;
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 8fc234a62b1..5919bf54468 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -22,7 +22,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def update
- @group_member = @group.group_members.find(params[:id])
+ @group_member = @group.members_and_requesters.find(params[:id])
return render_403 unless can?(current_user, :update_group_member, @group_member)
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index d925dcd21ff..5a01a59481b 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -26,7 +26,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def update
- @project_member = @project.project_members.find(params[:id])
+ @project_member = @project.members_and_requesters.find(params[:id])
return render_403 unless can?(current_user, :update_project_member, @project_member)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3882fa4791d..8e9d6766d80 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -272,7 +272,7 @@ class ProjectsController < Projects::ApplicationController
render 'projects/empty' if @project.empty_repo?
else
- if @project.wiki_enabled?
+ if can?(current_user, :read_wiki, @project)
@project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 8e822ed0ea2..aaee6eaeedd 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -58,7 +58,7 @@ module PreferencesHelper
user_view
elsif user_view == "activity"
"activity"
- elsif @project.wiki_enabled?
+ elsif can?(current_user, :read_wiki, @project)
"wiki"
elsif @project.feature_available?(:issues, current_user)
"projects/issues/issues"
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 33db197e612..bbda848c39d 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -10,6 +10,9 @@ class Issue < ActiveRecord::Base
include RelativePositioning
include TimeTrackable
include ThrottledTouch
+ include IgnorableColumn
+
+ ignore_column :assignee_id
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
diff --git a/app/models/user.rb b/app/models/user.rb
index 093ff808626..92b461ce3ed 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -315,6 +315,8 @@ class User < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def search(query)
+ query = query.downcase
+
order = <<~SQL
CASE
WHEN users.name = %{query} THEN 0
@@ -324,8 +326,11 @@ class User < ActiveRecord::Base
END
SQL
- fuzzy_search(query, [:name, :email, :username])
- .reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
+ where(
+ fuzzy_arel_match(:name, query)
+ .or(fuzzy_arel_match(:username, query))
+ .or(arel_table[:email].eq(query))
+ ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
end
# searches user by given pattern
@@ -333,15 +338,17 @@ class User < ActiveRecord::Base
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
def search_with_secondary_emails(query)
+ query = query.downcase
+
email_table = Email.arel_table
matched_by_emails_user_ids = email_table
.project(email_table[:user_id])
- .where(Email.fuzzy_arel_match(:email, query))
+ .where(email_table[:email].eq(query))
where(
fuzzy_arel_match(:name, query)
- .or(fuzzy_arel_match(:email, query))
.or(fuzzy_arel_match(:username, query))
+ .or(arel_table[:email].eq(query))
.or(arel_table[:id].in(matched_by_emails_user_ids))
)
end
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 0ec07605631..cb8db306b56 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -1,4 +1,4 @@
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to admin_root_path, title: 'Admin Overview' do
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 0bf318b0b66..0c27b09f7b1 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -1,7 +1,7 @@
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to group_path(@group), title: @group.name do
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 7e23f9c1f05..a5a62a0695f 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -1,4 +1,4 @@
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to profile_path, title: 'Profile Settings' do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 53a9162b703..be39f577ba7 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -1,4 +1,4 @@
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
- can_edit = can?(current_user, :admin_project, @project)
.context-header
diff --git a/changelogs/unreleased/15832-fix-access-level-update-for-requesters.yml b/changelogs/unreleased/15832-fix-access-level-update-for-requesters.yml
new file mode 100644
index 00000000000..9d6c958cb3e
--- /dev/null
+++ b/changelogs/unreleased/15832-fix-access-level-update-for-requesters.yml
@@ -0,0 +1,5 @@
+---
+title: Fix error that was preventing users to change the access level of access requests for Groups or Projects
+merge_request: 15832
+author:
+type: fixed
diff --git a/changelogs/unreleased/35724-animate-sidebar.yml b/changelogs/unreleased/35724-animate-sidebar.yml
new file mode 100644
index 00000000000..5d0b46a23c8
--- /dev/null
+++ b/changelogs/unreleased/35724-animate-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Animate contextual sidebar on collapse/expand
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-remove-allocation-tracking-influxdb.yml b/changelogs/unreleased/sh-remove-allocation-tracking-influxdb.yml
new file mode 100644
index 00000000000..b98573df303
--- /dev/null
+++ b/changelogs/unreleased/sh-remove-allocation-tracking-influxdb.yml
@@ -0,0 +1,5 @@
+---
+title: Remove allocation tracking code from InfluxDB sampler for performance
+merge_request:
+author:
+type: performance
diff --git a/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
index 477b2106dea..21b367711c3 100644
--- a/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
+++ b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
class RemoveDeprecatedIssuesTrackerColumnsFromProjects < ActiveRecord::Migration
def change
remove_column :projects, :issues_tracker, :string, default: 'gitlab', null: false
diff --git a/db/migrate/20160610301627_remove_notification_level_from_users.rb b/db/migrate/20160610301627_remove_notification_level_from_users.rb
index 8afb14df2cf..356e53b4b23 100644
--- a/db/migrate/20160610301627_remove_notification_level_from_users.rb
+++ b/db/migrate/20160610301627_remove_notification_level_from_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
class RemoveNotificationLevelFromUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb
index 52a9819c628..058bd539e65 100644
--- a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb
+++ b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb
index 4a7bde7f9f3..d0e5da4d28b 100644
--- a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb
+++ b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
index e28ab31d629..baf254c3bcc 100644
--- a/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
+++ b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
index aec709aaf59..9eafd8b9477 100644
--- a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
+++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
index df7d922b816..f32167037e0 100644
--- a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
+++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20161018024550_remove_priority_from_labels.rb b/db/migrate/20161018024550_remove_priority_from_labels.rb
index b7416cca664..bc25a43526c 100644
--- a/db/migrate/20161018024550_remove_priority_from_labels.rb
+++ b/db/migrate/20161018024550_remove_priority_from_labels.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
class RemovePriorityFromLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb
index 82fbdf02444..a547409aaa5 100644
--- a/db/migrate/20161201160452_migrate_project_statistics.rb
+++ b/db/migrate/20161201160452_migrate_project_statistics.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
class MigrateProjectStatistics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170222143500_remove_old_project_id_columns.rb b/db/migrate/20170222143500_remove_old_project_id_columns.rb
index 268144a2552..9bed38a3444 100644
--- a/db/migrate/20170222143500_remove_old_project_id_columns.rb
+++ b/db/migrate/20170222143500_remove_old_project_id_columns.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# rubocop:disable RemoveIndex
class RemoveOldProjectIdColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
index 1a77d5934a3..0535c2ddaf2 100644
--- a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
+++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# rubocop:disable Migration/Datetime
class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
index 807dfcb385d..9b9098d115d 100644
--- a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
+++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# rubocop:disable Migration/UpdateLargeTable
class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20171106154015_remove_issues_branch_name.rb b/db/migrate/20171106154015_remove_issues_branch_name.rb
index 3d08225c96d..162b6bafab4 100644
--- a/db/migrate/20171106154015_remove_issues_branch_name.rb
+++ b/db/migrate/20171106154015_remove_issues_branch_name.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/post_migrate/20170523073948_remove_assignee_id_from_issue.rb b/db/post_migrate/20170523073948_remove_assignee_id_from_issue.rb
new file mode 100644
index 00000000000..006d17b4d62
--- /dev/null
+++ b/db/post_migrate/20170523073948_remove_assignee_id_from_issue.rb
@@ -0,0 +1,48 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveAssigneeIdFromIssue < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+
+ include ::EachBatch
+ end
+
+ def up
+ remove_column :issues, :assignee_id
+ end
+
+ def down
+ add_column :issues, :assignee_id, :integer
+ add_concurrent_index :issues, :assignee_id
+
+ update_value = Arel.sql('(SELECT user_id FROM issue_assignees WHERE issue_assignees.issue_id = issues.id LIMIT 1)')
+
+ # This is only used in the down step, so we can ignore the RuboCop warning
+ # about large tables, as this is very unlikely to be run on GitLab.com
+ update_column_in_batches(:issues, :assignee_id, update_value) # rubocop:disable Migration/UpdateLargeTable
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c0a141885ad..f0b1da16d53 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -841,7 +841,6 @@ ActiveRecord::Schema.define(version: 20171206221519) do
create_table "issues", force: :cascade do |t|
t.string "title"
- t.integer "assignee_id"
t.integer "author_id"
t.integer "project_id"
t.datetime "created_at"
@@ -867,7 +866,6 @@ ActiveRecord::Schema.define(version: 20171206221519) do
t.datetime_with_timezone "closed_at"
end
- add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree
add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
diff --git a/doc/administration/index.md b/doc/administration/index.md
index c8d28d8485a..e6986a2b07f 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -35,7 +35,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Raketasks](../raketasks/README.md): Perform various tasks for maintenance, backups, automatic webhooks setup, etc.
- [Backup and restore](../raketasks/backup_restore.md): Backup and restore your GitLab instance.
-- [Operations](operations.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq Job throttling, Sidekiq MemoryKiller, Unicorn).
+- [Operations](operations/index.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq Job throttling, Sidekiq MemoryKiller, Unicorn).
- [Restart GitLab](restart_gitlab.md): Learn how to restart GitLab and its components.
#### Updating GitLab
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 11d5e077a36..f495990d9a4 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -45,8 +45,9 @@ In this experimental phase, only a few metrics are available:
| redis_ping_success | Gauge | 9.4 | Whether or not the last redis ping succeeded |
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
-| filesystem_circuitbreaker_latency_seconds | Histogram | 9.5 | Latency of the stat check the circuitbreaker uses to probe a shard |
+| filesystem_circuitbreaker_latency_seconds | Gauge | 9.5 | Time spent validating if a storage is accessible |
| filesystem_circuitbreaker | Gauge | 9.5 | Wether or not the circuit for a certain shard is broken or not |
+| circuitbreaker_storage_check_duration_seconds | Histogram | 10.3 | Time a single storage probe took |
## Metrics shared directory
diff --git a/doc/administration/operations.md b/doc/administration/operations.md
index 0daceb98d99..4797d2a3206 100644
--- a/doc/administration/operations.md
+++ b/doc/administration/operations.md
@@ -1,7 +1 @@
-# GitLab operations
-
-- [Sidekiq MemoryKiller](operations/sidekiq_memory_killer.md)
-- [Sidekiq Job throttling](operations/sidekiq_job_throttling.md)
-- [Cleaning up Redis sessions](operations/cleaning_up_redis_sessions.md)
-- [Understanding Unicorn and unicorn-worker-killer](operations/unicorn.md)
-- [Moving repositories to a new location](operations/moving_repositories.md)
+This document was moved to [another location](operations/index.md).
diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md
new file mode 100644
index 00000000000..320d71a9527
--- /dev/null
+++ b/doc/administration/operations/index.md
@@ -0,0 +1,16 @@
+# Performing Operations in GitLab
+
+Keep your GitLab instance up and running smoothly.
+
+- [Clean up Redis sessions](cleaning_up_redis_sessions.md): Prior to GitLab 7.3,
+user sessions did not automatically expire from Redis. If
+you have been running a large GitLab server (thousands of users) since before
+GitLab 7.3 we recommend cleaning up stale sessions to compact the Redis
+database after you upgrade to GitLab 7.3.
+- [Moving repositories](moving_repositories.md): Moving all repositories managed
+by GitLab to another file system or another server.
+- [Sidekiq job throttling](sidekiq_job_throttling.md): Throttle Sidekiq queues
+that to prioritize important jobs.
+- [Sidekiq MemoryKiller](sidekiq_memory_killer.md): Configure Sidekiq MemoryKiller
+to restart Sidekiq.
+- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer. \ No newline at end of file
diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md
index 9e59ddc8cce..4b9791c95bc 100644
--- a/doc/development/automatic_ce_ee_merge.md
+++ b/doc/development/automatic_ce_ee_merge.md
@@ -17,7 +17,7 @@ someone who is familiar with the code you touched.
[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream
-### Always merge EE merge requests before their CE counterparts
+## Always merge EE merge requests before their CE counterparts
**In order to avoid conflicts in the CE->EE merge, you should always merge the
EE version of your CE merge request first, if present.**
diff --git a/doc/operations/README.md b/doc/operations/README.md
index 58f16aff7bd..d7a83948b87 100644
--- a/doc/operations/README.md
+++ b/doc/operations/README.md
@@ -1 +1 @@
-This document was moved to [administration/operations](../administration/operations.md).
+This document was moved to [another location](../administration/operations/index.md).
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index a2d9a0332e0..753694a5392 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -138,7 +138,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
private
def assigned_to_me(key)
- project.send(key).where(assignee_id: current_user.id)
+ project.send(key).assigned_to(current_user)
end
def project
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index d96e7f2770f..928706dfda7 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -248,8 +248,21 @@ module API
end
class GroupDetail < Group
- expose :projects, using: Entities::Project
- expose :shared_projects, using: Entities::Project
+ expose :projects, using: Entities::Project do |group, options|
+ GroupProjectsFinder.new(
+ group: group,
+ current_user: options[:current_user],
+ options: { only_owned: true }
+ ).execute
+ end
+
+ expose :shared_projects, using: Entities::Project do |group, options|
+ GroupProjectsFinder.new(
+ group: group,
+ current_user: options[:current_user],
+ options: { only_shared: true }
+ ).execute
+ end
end
class Commit < Grape::Entity
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index e60e00d7956..5f943ba27d1 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -161,6 +161,8 @@ module API
use :issue_params
end
post ':id/issues' do
+ authorize! :create_issue, user_project
+
# Setting created_at time only allowed for admins and project owners
unless current_user.admin? || user_project.owner == current_user
params.delete(:created_at)
diff --git a/lib/gitlab/git/storage/checker.rb b/lib/gitlab/git/storage/checker.rb
index de63cb4b40c..d3c37f82101 100644
--- a/lib/gitlab/git/storage/checker.rb
+++ b/lib/gitlab/git/storage/checker.rb
@@ -5,6 +5,8 @@ module Gitlab
include CircuitBreakerSettings
attr_reader :storage_path, :storage, :hostname, :logger
+ METRICS_MUTEX = Mutex.new
+ STORAGE_TIMING_BUCKETS = [0.1, 0.15, 0.25, 0.33, 0.5, 1, 1.5, 2.5, 5, 10, 15].freeze
def self.check_all(logger = Rails.logger)
threads = Gitlab.config.repositories.storages.keys.map do |storage_name|
@@ -19,6 +21,17 @@ module Gitlab
end
end
+ def self.check_histogram
+ @check_histogram ||=
+ METRICS_MUTEX.synchronize do
+ @check_histogram || Gitlab::Metrics.histogram(:circuitbreaker_storage_check_duration_seconds,
+ 'Storage check time in seconds',
+ {},
+ STORAGE_TIMING_BUCKETS
+ )
+ end
+ end
+
def initialize(storage, logger = Rails.logger)
@storage = storage
config = Gitlab.config.repositories.storages[@storage]
@@ -45,7 +58,7 @@ module Gitlab
end
def check
- if Gitlab::Git::Storage::ForkedStorageCheck.storage_available?(storage_path, storage_timeout, access_retries)
+ if perform_access_check
track_storage_accessible
true
else
@@ -57,6 +70,15 @@ module Gitlab
private
+ def perform_access_check
+ start_time = Gitlab::Metrics::System.monotonic_time
+
+ Gitlab::Git::Storage::ForkedStorageCheck.storage_available?(storage_path, storage_timeout, access_retries)
+ ensure
+ execution_time = Gitlab::Metrics::System.monotonic_time - start_time
+ self.class.check_histogram.observe({ storage: storage }, execution_time)
+ end
+
def track_storage_inaccessible
first_failure = current_failure_info.first_failure || Time.now
last_failure = Time.now
diff --git a/lib/gitlab/metrics/samplers/influx_sampler.rb b/lib/gitlab/metrics/samplers/influx_sampler.rb
index f4f9b5ca792..5a0f7f28fc8 100644
--- a/lib/gitlab/metrics/samplers/influx_sampler.rb
+++ b/lib/gitlab/metrics/samplers/influx_sampler.rb
@@ -27,7 +27,6 @@ module Gitlab
def sample
sample_memory_usage
sample_file_descriptors
- sample_objects
sample_gc
flush
@@ -48,29 +47,6 @@ module Gitlab
add_metric('file_descriptors', value: System.file_descriptor_count)
end
- if Metrics.mri?
- def sample_objects
- sample = Allocations.to_hash
- counts = sample.each_with_object({}) do |(klass, count), hash|
- name = klass.name
-
- next unless name
-
- hash[name] = count
- end
-
- # Symbols aren't allocated so we'll need to add those manually.
- counts['Symbol'] = Symbol.all_symbols.length
-
- counts.each do |name, count|
- add_metric('object_counts', { count: count }, type: name)
- end
- end
- else
- def sample_objects
- end
- end
-
def sample_gc
time = GC::Profiler.total_time * 1000.0
stats = GC.stat.merge(total_time: time)
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index f4901be9581..b68800417a2 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -48,7 +48,6 @@ module Gitlab
def sample
start_time = System.monotonic_time
sample_gc
- sample_objects
metrics[:memory_usage].set(labels, System.memory_usage)
metrics[:file_descriptors].set(labels, System.file_descriptor_count)
@@ -68,32 +67,6 @@ module Gitlab
end
end
- def sample_objects
- list_objects.each do |name, count|
- metrics[:objects_total].set(labels.merge(class: name), count)
- end
- end
-
- if Metrics.mri?
- def list_objects
- sample = Allocations.to_hash
- counts = sample.each_with_object({}) do |(klass, count), hash|
- name = klass.name
-
- next unless name
-
- hash[name] = count
- end
-
- # Symbols aren't allocated so we'll need to add those manually.
- counts['Symbol'] = Symbol.all_symbols.length
- counts
- end
- else
- def list_objects
- end
- end
-
def worker_label
return {} unless defined?(Unicorn::Worker)
diff --git a/rubocop/cop/migration/remove_column.rb b/rubocop/cop/migration/remove_column.rb
new file mode 100644
index 00000000000..e53eb2e07b2
--- /dev/null
+++ b/rubocop/cop/migration/remove_column.rb
@@ -0,0 +1,30 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if remove_column is used in a regular (not
+ # post-deployment) migration.
+ class RemoveColumn < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = '`remove_column` must only be used in post-deployment migrations'.freeze
+
+ def on_def(node)
+ def_method = node.children[0]
+
+ return unless in_migration?(node) && !in_post_deployment_migration?(node)
+ return unless def_method == :change || def_method == :up
+
+ node.each_descendant(:send) do |send_node|
+ send_method = send_node.children[1]
+
+ if send_method == :remove_column
+ add_offense(send_node, :selector)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/migration_helpers.rb b/rubocop/migration_helpers.rb
index c3473771178..c066d424437 100644
--- a/rubocop/migration_helpers.rb
+++ b/rubocop/migration_helpers.rb
@@ -7,5 +7,11 @@ module RuboCop
dirname.end_with?('db/migrate', 'db/post_migrate')
end
+
+ def in_post_deployment_migration?(node)
+ dirname = File.dirname(node.location.expression.source_buffer.name)
+
+ dirname.end_with?('db/post_migrate')
+ end
end
end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 7621ea50da9..eb52be3d731 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -14,6 +14,7 @@ require_relative 'cop/migration/add_index'
require_relative 'cop/migration/add_timestamps'
require_relative 'cop/migration/datetime'
require_relative 'cop/migration/hash_index'
+require_relative 'cop/migration/remove_column'
require_relative 'cop/migration/remove_concurrent_index'
require_relative 'cop/migration/remove_index'
require_relative 'cop/migration/reversible_add_column_with_default'
diff --git a/scripts/trigger-build-omnibus b/scripts/trigger-build-omnibus
index 3c5c22c9372..4ff0e8e10b7 100755
--- a/scripts/trigger-build-omnibus
+++ b/scripts/trigger-build-omnibus
@@ -66,7 +66,7 @@ module Omnibus
raise 'Pipeline timeout!' if timeout?
case status
- when :pending, :running
+ when :created, :pending, :running
puts "Waiting another #{INTERVAL} seconds ..."
sleep INTERVAL
when :success
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 9c6d584f59b..362d5cc4514 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -62,6 +62,25 @@ describe Groups::GroupMembersController do
end
end
+ describe 'PUT update' do
+ let(:requester) { create(:group_member, :access_request, group: group) }
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ Gitlab::Access.options.each do |label, value|
+ it "can change the access level to #{label}" do
+ xhr :put, :update, group_member: { access_level: value },
+ group_id: group,
+ id: requester
+
+ expect(requester.reload.human_access).to eq(label)
+ end
+ end
+ end
+
describe 'DELETE destroy' do
let(:member) { create(:group_member, :developer, group: group) }
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index a34dc27a5ed..290dba0610a 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -66,6 +66,26 @@ describe Projects::ProjectMembersController do
end
end
+ describe 'PUT update' do
+ let(:requester) { create(:project_member, :access_request, project: project) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ Gitlab::Access.options.each do |label, value|
+ it "can change the access level to #{label}" do
+ xhr :put, :update, project_member: { access_level: value },
+ namespace_id: project.namespace,
+ project_id: project,
+ id: requester
+
+ expect(requester.reload.human_access).to eq(label)
+ end
+ end
+ end
+
describe 'DELETE destroy' do
let(:member) { create(:project_member, :developer, project: project) }
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 4000cd085b7..8ace424f8af 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -58,6 +58,10 @@ FactoryGirl.define do
end
end
+ trait :readme do
+ project_view :readme
+ end
+
factory :omniauth_user do
transient do
extern_uid '123456'
diff --git a/spec/features/groups/members/manage_members.rb b/spec/features/groups/members/manage_members.rb
index da1e17225db..21f7b4999ad 100644
--- a/spec/features/groups/members/manage_members.rb
+++ b/spec/features/groups/members/manage_members.rb
@@ -38,6 +38,27 @@ feature 'Groups > Members > Manage members' do
end
end
+ scenario 'do not disclose email addresses', :js do
+ group.add_owner(user1)
+ create(:user, email: 'undisclosed_email@gitlab.com', name: "Jane 'invisible' Doe")
+
+ visit group_group_members_path(group)
+
+ find('.select2-container').click
+ select_input = find('.select2-input')
+
+ select_input.send_keys('@gitlab.com')
+ wait_for_requests
+
+ expect(page).to have_content('No matches found')
+
+ select_input.native.clear
+ select_input.send_keys('undisclosed_email@gitlab.com')
+ wait_for_requests
+
+ expect(page).to have_content("Jane 'invisible' Doe")
+ end
+
scenario 'remove user from group', :js do
group.add_owner(user1)
group.add_developer(user2)
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 8b8080563d3..749aa25e632 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -77,15 +77,6 @@ describe PreferencesHelper do
end
end
- def stub_user(messages = {})
- if messages.empty?
- allow(helper).to receive(:current_user).and_return(nil)
- else
- allow(helper).to receive(:current_user)
- .and_return(double('user', messages))
- end
- end
-
describe '#default_project_view' do
context 'user not signed in' do
before do
@@ -125,5 +116,70 @@ describe PreferencesHelper do
end
end
end
+
+ context 'user signed in' do
+ let(:user) { create(:user, :readme) }
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'when the user is allowed to see the code' do
+ it 'returns the project view' do
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(true)
+
+ expect(helper.default_project_view).to eq('readme')
+ end
+ end
+
+ context 'with wikis enabled and the right policy for the user' do
+ before do
+ project.project_feature.update_attribute(:issues_access_level, 0)
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
+ end
+
+ it 'returns wiki if the user has the right policy' do
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(true)
+
+ expect(helper.default_project_view).to eq('wiki')
+ end
+
+ it 'returns customize_workflow if the user does not have the right policy' do
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('customize_workflow')
+ end
+ end
+
+ context 'with issues as a feature available' do
+ it 'return issues' do
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('projects/issues/issues')
+ end
+ end
+
+ context 'with no activity, no wikies and no issues' do
+ it 'returns customize_workflow as default' do
+ project.project_feature.update_attribute(:issues_access_level, 0)
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('customize_workflow')
+ end
+ end
+ end
+ end
+
+ def stub_user(messages = {})
+ if messages.empty?
+ allow(helper).to receive(:current_user).and_return(nil)
+ else
+ allow(helper).to receive(:current_user)
+ .and_return(double('user', messages))
+ end
end
end
diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
index 4f20e31f511..a3fa07d5bc2 100644
--- a/spec/javascripts/fly_out_nav_spec.js
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -253,7 +253,7 @@ describe('Fly out sidebar navigation', () => {
it('shows collapsed only sub-items if icon only sidebar', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-icons-only');
+ sidebar.classList.add('sidebar-collapsed-desktop');
subItems.classList.add('is-fly-out-only');
setSidebar(sidebar);
@@ -343,7 +343,7 @@ describe('Fly out sidebar navigation', () => {
it('returns true when active & collapsed sidebar', () => {
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-icons-only');
+ sidebar.classList.add('sidebar-collapsed-desktop');
el.classList.add('active');
setSidebar(sidebar);
diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/issue_note_spec.js
index 73fd188dbe5..bd888b2cbae 100644
--- a/spec/javascripts/notes/components/issue_note_spec.js
+++ b/spec/javascripts/notes/components/issue_note_spec.js
@@ -41,4 +41,19 @@ describe('issue_note', () => {
it('should render issue body', () => {
expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
});
+
+ it('prevents note preview xss', (done) => {
+ const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
+ const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`;
+ const alertSpy = spyOn(window, 'alert');
+ vm.updateNote = () => new Promise($.noop);
+
+ vm.formUpdateHandler(noteBody, null, $.noop);
+
+ setTimeout(() => {
+ expect(alertSpy).not.toHaveBeenCalled();
+ expect(vm.note.note_html).toEqual(_.escape(noteBody));
+ done();
+ }, 0);
+ });
});
diff --git a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
index 667e4747897..f66451c5188 100644
--- a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
@@ -21,7 +21,6 @@ describe Gitlab::Metrics::Samplers::InfluxSampler do
it 'samples various statistics' do
expect(sampler).to receive(:sample_memory_usage)
expect(sampler).to receive(:sample_file_descriptors)
- expect(sampler).to receive(:sample_objects)
expect(sampler).to receive(:sample_gc)
expect(sampler).to receive(:flush)
@@ -72,28 +71,6 @@ describe Gitlab::Metrics::Samplers::InfluxSampler do
end
end
- if Gitlab::Metrics.mri?
- describe '#sample_objects' do
- it 'adds a metric containing the amount of allocated objects' do
- expect(sampler).to receive(:add_metric)
- .with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash))
- .at_least(:once)
- .and_call_original
-
- sampler.sample_objects
- end
-
- it 'ignores classes without a name' do
- expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
-
- expect(sampler).not_to receive(:add_metric)
- .with('object_counts', an_instance_of(Hash), type: nil)
-
- sampler.sample_objects
- end
- end
- end
-
describe '#sample_gc' do
it 'adds a metric containing garbage collection statistics' do
expect(GC::Profiler).to receive(:total_time).and_return(0.24)
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 53699327da1..375cbf8a9ca 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -11,7 +11,6 @@ describe Gitlab::Metrics::Samplers::RubySampler do
it 'samples various statistics' do
expect(Gitlab::Metrics::System).to receive(:memory_usage)
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
- expect(sampler).to receive(:sample_objects)
expect(sampler).to receive(:sample_gc)
sampler.sample
@@ -65,26 +64,4 @@ describe Gitlab::Metrics::Samplers::RubySampler do
sampler.sample
end
end
-
- if Gitlab::Metrics.mri?
- describe '#sample_objects' do
- it 'adds a metric containing the amount of allocated objects' do
- expect(sampler.metrics[:objects_total]).to receive(:set)
- .with(include(class: anything), be > 0)
- .at_least(:once)
- .and_call_original
-
- sampler.sample
- end
-
- it 'ignores classes without a name' do
- expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
-
- expect(sampler.metrics[:objects_total]).not_to receive(:set)
- .with(include(class: 'object_counts'), anything)
-
- sampler.sample
- end
- end
- end
end
diff --git a/spec/migrations/remove_assignee_id_from_issue_spec.rb b/spec/migrations/remove_assignee_id_from_issue_spec.rb
new file mode 100644
index 00000000000..2c6f992d3ae
--- /dev/null
+++ b/spec/migrations/remove_assignee_id_from_issue_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170523073948_remove_assignee_id_from_issue.rb')
+
+describe RemoveAssigneeIdFromIssue, :migration do
+ let(:issues) { table(:issues) }
+ let(:issue_assignees) { table(:issue_assignees) }
+ let(:users) { table(:users) }
+
+ let!(:user_1) { users.create(email: 'email1@example.com') }
+ let!(:user_2) { users.create(email: 'email2@example.com') }
+ let!(:user_3) { users.create(email: 'email3@example.com') }
+
+ def create_issue(assignees:)
+ issues.create.tap do |issue|
+ assignees.each do |assignee|
+ issue_assignees.create(issue_id: issue.id, user_id: assignee.id)
+ end
+ end
+ end
+
+ let!(:issue_single_assignee) { create_issue(assignees: [user_1]) }
+ let!(:issue_no_assignee) { create_issue(assignees: []) }
+ let!(:issue_multiple_assignees) { create_issue(assignees: [user_2, user_3]) }
+
+ describe '#down' do
+ it 'sets the assignee_id to a random matching assignee from the assignees table' do
+ migrate!
+ disable_migrations_output { described_class.new.down }
+
+ expect(issue_single_assignee.reload.assignee_id).to eq(user_1.id)
+ expect(issue_no_assignee.reload.assignee_id).to be_nil
+ expect(issue_multiple_assignees.reload.assignee_id).to eq(user_2.id).or(user_3.id)
+
+ disable_migrations_output { described_class.new.up }
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index cdabd35b6ba..4687d9dfa00 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -913,11 +913,11 @@ describe User do
describe 'email matching' do
it 'returns users with a matching Email' do
- expect(described_class.search(user.email)).to eq([user, user2])
+ expect(described_class.search(user.email)).to eq([user])
end
- it 'returns users with a partially matching Email' do
- expect(described_class.search(user.email[0..2])).to eq([user, user2])
+ it 'does not return users with a partially matching Email' do
+ expect(described_class.search(user.email[0..2])).not_to include(user, user2)
end
it 'returns users with a matching Email regardless of the casing' do
@@ -973,8 +973,8 @@ describe User do
expect(search_with_secondary_emails(user.email)).to eq([user])
end
- it 'returns users with a partially matching email' do
- expect(search_with_secondary_emails(user.email[0..2])).to eq([user])
+ it 'does not return users with a partially matching email' do
+ expect(search_with_secondary_emails(user.email[0..2])).not_to include([user])
end
it 'returns users with a matching email regardless of the casing' do
@@ -997,29 +997,8 @@ describe User do
expect(search_with_secondary_emails(email.email)).to eq([email.user])
end
- it 'returns users with a matching part of secondary email' do
- expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user])
- end
-
- it 'return users with a matching part of secondary email regardless of case' do
- expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user])
- expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user])
- expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user])
- end
-
- it 'returns multiple users with matching secondary emails' do
- email1 = create(:email, email: '1_testemail@example.com')
- email2 = create(:email, email: '2_testemail@example.com')
- email3 = create(:email, email: 'other@email.com')
- email3.user.update_attributes!(email: 'another@mail.com')
-
- expect(
- search_with_secondary_emails('testemail@example.com').map(&:id)
- ).to include(email1.user.id, email2.user.id)
-
- expect(
- search_with_secondary_emails('testemail@example.com').map(&:id)
- ).not_to include(email3.user.id)
+ it 'does not return users with a matching part of secondary email' do
+ expect(search_with_secondary_emails(email.email[1..4])).not_to include([email.user])
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 554723d6b1e..6330c140246 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -173,6 +173,28 @@ describe API::Groups do
end
describe "GET /groups/:id" do
+ # Given a group, create one project for each visibility level
+ #
+ # group - Group to add projects to
+ # share_with - If provided, each project will be shared with this Group
+ #
+ # Returns a Hash of visibility_level => Project pairs
+ def add_projects_to_group(group, share_with: nil)
+ projects = {
+ public: create(:project, :public, namespace: group),
+ internal: create(:project, :internal, namespace: group),
+ private: create(:project, :private, namespace: group)
+ }
+
+ if share_with
+ create(:project_group_link, project: projects[:public], group: share_with)
+ create(:project_group_link, project: projects[:internal], group: share_with)
+ create(:project_group_link, project: projects[:private], group: share_with)
+ end
+
+ projects
+ end
+
context 'when unauthenticated' do
it 'returns 404 for a private group' do
get api("/groups/#{group2.id}")
@@ -183,6 +205,26 @@ describe API::Groups do
get api("/groups/#{group1.id}")
expect(response).to have_gitlab_http_status(200)
end
+
+ it 'returns only public projects in the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group)
+
+ get api("/groups/#{public_group.id}")
+
+ expect(json_response['projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id)
+ end
+
+ it 'returns only public projects shared with the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group, share_with: group1)
+
+ get api("/groups/#{group1.id}")
+
+ expect(json_response['shared_projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id)
+ end
end
context "when authenticated as user" do
@@ -222,6 +264,26 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(404)
end
+
+ it 'returns only public and internal projects in the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group)
+
+ get api("/groups/#{public_group.id}", user2)
+
+ expect(json_response['projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id, projects[:internal].id)
+ end
+
+ it 'returns only public and internal projects shared with the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group, share_with: group1)
+
+ get api("/groups/#{group1.id}", user2)
+
+ expect(json_response['shared_projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id, projects[:internal].id)
+ end
end
context "when authenticated as admin" do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 99525cd0a6a..3f5070a1fd2 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -860,6 +860,20 @@ describe API::Issues, :mailer do
end
end
+ context 'user does not have permissions to create issue' do
+ let(:not_member) { create(:user) }
+
+ before do
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'renders 403' do
+ post api("/projects/#{project.id}/issues", not_member), title: 'new issue'
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3,
diff --git a/spec/rubocop/cop/migration/remove_column_spec.rb b/spec/rubocop/cop/migration/remove_column_spec.rb
new file mode 100644
index 00000000000..89112f01723
--- /dev/null
+++ b/spec/rubocop/cop/migration/remove_column_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/remove_column'
+
+describe RuboCop::Cop::Migration::RemoveColumn do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ def source(meth = 'change')
+ "def #{meth}; remove_column :table, :column; end"
+ end
+
+ context 'in a regular migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ allow(cop).to receive(:in_post_deployment_migration?).and_return(false)
+ end
+
+ it 'registers an offense when remove_column is used in the change method' do
+ inspect_source(cop, source('change'))
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+
+ it 'registers an offense when remove_column is used in the up method' do
+ inspect_source(cop, source('up'))
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+
+ it 'registers no offense when remove_column is used in the down method' do
+ inspect_source(cop, source('down'))
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+
+ context 'in a post-deployment migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ allow(cop).to receive(:in_post_deployment_migration?).and_return(true)
+ end
+
+ it 'registers no offense' do
+ inspect_source(cop, source)
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+
+ context 'outside of a migration' do
+ it 'registers no offense' do
+ inspect_source(cop, source)
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb
index 2d04d824180..d4ef31c0c74 100644
--- a/spec/services/members/authorized_destroy_service_spec.rb
+++ b/spec/services/members/authorized_destroy_service_spec.rb
@@ -45,7 +45,7 @@ describe Members::AuthorizedDestroyService do
expect { described_class.new(member, member_user).execute }
.to change { number_of_assigned_issuables(member_user) }.from(4).to(2)
- expect(issue.reload.assignee_id).to be_nil
+ expect(issue.reload.assignee_ids).to be_empty
expect(merge_request.reload.assignee_id).to be_nil
end
end
diff --git a/spec/services/users/keys_count_service_spec.rb b/spec/services/users/keys_count_service_spec.rb
index a188cf86772..bee8380e8b7 100644
--- a/spec/services/users/keys_count_service_spec.rb
+++ b/spec/services/users/keys_count_service_spec.rb
@@ -15,14 +15,12 @@ describe Users::KeysCountService, :use_clean_rails_memory_store_caching do
expect(service.count).to eq(1)
end
- it 'caches the number of keys in Redis' do
+ it 'caches the number of keys in Redis', :request_store do
+ service.delete_cache
+ control_count = ActiveRecord::QueryRecorder.new { service.count }.count
service.delete_cache
- recorder = ActiveRecord::QueryRecorder.new do
- 2.times { service.count }
- end
-
- expect(recorder.count).to eq(1)
+ expect { 2.times { service.count } }.not_to exceed_query_limit(control_count)
end
end