Commit 8c543e60 authored by Achilleas Pipinellis's avatar Achilleas Pipinellis

Merge branch 'master' into gitlab-ci-yaml-updates

parents 388f69b0 a3b626bf
......@@ -15,6 +15,7 @@
.sass-cache/
.secret
.vagrant
.byebug_history
Vagrantfile
backups/*
config/aws.yml
......
......@@ -71,15 +71,6 @@ spec:services:
- ruby
- mysql
spec:benchmark:
stage: test
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
tags:
- ruby
- mysql
allow_failure: true
spec:other:
stage: test
script:
......@@ -243,22 +234,6 @@ spec:services:ruby22:
- ruby
- mysql
spec:benchmark:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
cache:
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
allow_failure: true
spec:other:ruby22:
stage: test
image: ruby:2.2
......
......@@ -9,6 +9,7 @@ v 8.6.0 (unreleased)
- Fix issue when pushing to projects ending in .wiki
- Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
- Don't load all of GitLab in mail_room
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
- Strip leading and trailing spaces in URL validator (evuez)
......@@ -19,6 +20,7 @@ v 8.6.0 (unreleased)
- Allow to use YAML anchors when parsing the `.gitlab-ci.yml` (Pascal Bach)
- Ignore jobs that start with `.` (hidden jobs)
- Allow to pass name of created artifacts archive in `.gitlab-ci.yml`
- Refactor and greatly improve search performance
- Add support for cross-project label references
- Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users
......@@ -30,6 +32,7 @@ v 8.6.0 (unreleased)
- Show labels in dashboard and group milestone views
- Add main language of a project in the list of projects (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages
- Move group activity to separate page
v 8.5.5
- Ensure removing a project removes associated Todo entries
......
......@@ -30,7 +30,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.2.0'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-saml', '~> 1.4.2'
gem 'omniauth-saml', '~> 1.5.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
......
......@@ -358,7 +358,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
gitlab_git (9.0.0)
gitlab_git (9.0.1)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -532,8 +532,8 @@ GEM
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
omniauth-saml (1.4.2)
omniauth (~> 1.1)
omniauth-saml (1.5.0)
omniauth (~> 1.3)
ruby-saml (~> 1.1, >= 1.1.1)
omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
......@@ -692,7 +692,7 @@ GEM
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-progressbar (1.7.5)
ruby-saml (1.1.1)
ruby-saml (1.1.2)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
ruby2ruby (2.2.0)
......@@ -975,7 +975,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.2.0)
omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.4.2)
omniauth-saml (~> 1.5.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
......
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
$(".add-award").click (event) =>
$(".js-add-award").on "click", (event) =>
event.stopPropagation()
event.preventDefault()
......@@ -9,27 +9,46 @@ class @AwardsHandler
$("html").on 'click', (event) ->
if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible")
$(".emoji-menu").hide()
$(".emoji-menu").removeClass "is-visible"
$(".awards")
.off "click"
.on "click", ".js-emoji-btn", @handleClick
@renderFrequentlyUsedBlock()
@setupSearch()
handleClick: (e) ->
e.preventDefault()
emoji = $(this)
.find(".icon")
.data "emoji"
awards_handler.addAward emoji
showEmojiMenu: ->
if $(".emoji-menu").length
$(".emoji-menu").show()
if $(".emoji-menu").is ".is-visible"
$(".emoji-menu").removeClass "is-visible"
$("#emoji_search").blur()
else
$(".emoji-menu").addClass "is-visible"
$("#emoji_search").focus()
else
$.get "/emojis", (response) ->
$(".add-award").after response
$(".emoji-menu").show()
$('.js-add-award').addClass "is-loading"
$.get "/emojis", (response) =>
$('.js-add-award').removeClass "is-loading"
$(".js-award-holder").append response
setTimeout =>
$(".emoji-menu").addClass "is-visible"
$("#emoji_search").focus()
@setupSearch()
, 200
addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
$(".emoji-menu").hide()
$(".emoji-menu").removeClass "is-visible"
addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji)
......@@ -39,7 +58,7 @@ class @AwardsHandler
if @isActive(emoji)
@decrementCounter(emoji)
else
counter = @findEmojiIcon(emoji).siblings(".counter")
counter = @findEmojiIcon(emoji).siblings(".js-counter")
counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
......@@ -53,7 +72,7 @@ class @AwardsHandler
@findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter")
counter = @findEmojiIcon(emoji).siblings(".js-counter")
emojiIcon = counter.parent()
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
......@@ -70,9 +89,13 @@ class @AwardsHandler
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
authors = award_block.attr("data-original-title").split(", ")
authors = award_block
.attr("data-original-title")
.split(", ")
authors.splice(authors.indexOf("me"),1)
award_block.closest(".award").attr("data-original-title", authors.join(", "))
award_block
.closest(".js-emoji-btn")
.attr("data-original-title", authors.join(", "))
@resetTooltip(award_block)
addMeToAuthorList: (emoji) ->
......@@ -98,14 +121,18 @@ class @AwardsHandler
emojiCssClass = @resolveNameToCssClass(emoji)
nodes = []
nodes.push("<div class='award active' title='me'>")
nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>")
nodes.push("<div class='counter'>1</div>")
nodes.push("</div>")
emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji)
$(".award").tooltip()
nodes.push(
"<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>",
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
"<span class='award-control-text js-counter'>1</span>",
"</button>"
)
emoji_node = $(nodes.join("\n"))
.insertBefore(".js-award-holder")
.find(".emoji-icon")
.data("emoji", emoji)
$('.award-control').tooltip()
resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
......@@ -128,7 +155,7 @@ class @AwardsHandler
callback.call()
findEmojiIcon: (emoji) ->
$(".award [data-emoji='#{emoji}']")
$(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: ->
$('body, html').animate({
......@@ -164,13 +191,13 @@ class @AwardsHandler
term = $(ev.target).val()
# Clean previous search results
$("ul.emoji-search,h5.emoji-search").remove()
$("ul.emoji-menu-search, h5.emoji-search").remove()
if term
# Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search")
found_emojis = @searchEmojis(term).show()
ul = $("<ul>").addClass("emoji-search").append(found_emojis)
ul = $("<ul>").addClass("emoji-menu-list emoji-menu-search").append(found_emojis)
$(".emoji-menu-content ul, .emoji-menu-content h5").hide()
$(".emoji-menu-content").append(h5).append(ul)
else
......
......@@ -74,8 +74,9 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
new TreeView() if $('#tree-slider').length
when 'groups:show'
when 'groups:activity'
new Activities()
when 'groups:show'
shortcut_handler = new ShortcutsNavigation()
when 'groups:group_members:index'
new GroupMembers()
......
......@@ -157,3 +157,7 @@
float: right;
}
}
.content-block-small {
padding: 10px 0;
}
......@@ -294,7 +294,7 @@
}
.dropdown-content {
max-height: 200px;
max-height: 215px;
overflow-y: scroll;
}
......
......@@ -63,7 +63,7 @@
border-bottom: none;
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) {
@media (max-width: $screen-sm-max) {
width: 100%;
}
}
......
......@@ -152,3 +152,10 @@ $dropdown-toggle-border-color: #EAEAEA;
$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
$dropdown-toggle-icon-color: #C4C4C4;
$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
/*
* Award emoji
*/
$award-emoji-menu-bg: #FFF;
$award-emoji-menu-border: #F1F2F4;
$award-emoji-new-btn-icon-color: #DCDCDC;
.awards {
@include clearfix;
line-height: 34px;
.emoji-icon {
width: 20px;
height: 20px;
margin: 7px 0 0 5px;
}
.award {
@include border-radius(5px);
border: 1px solid;
padding: 0px 10px;
float: left;
margin-right: 5px;
border-color: $border-color;
cursor: pointer;
&:hover {
background-color: #dce0e5;
}
&.active {
border-color: $border-gray-light;
background-color: $gray-light;
&:hover {
background-color: #dce0e5;
}
.counter {
font-weight: bold;
}
}
.icon {
float: left;
margin-right: 10px;
}
.counter {
float: left;
}
}
.awards-controls {
position: relative;
margin-left: 10px;
float: left;
.add-award {
font-size: 24px;
color: $gl-gray;
position: relative;
top: 2px;
&:hover,
&:link {
text-decoration: none;
}
}
}
.emoji-menu{
.emoji-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 3px;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
text-align: left;
list-style: none;
background-color: #fff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0,0,0,.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
background-color: $award-emoji-menu-bg;
border: 1px solid $award-emoji-menu-border;
border-radius: $border-radius-base;
box-shadow: 0 6px 12px rgba(0,0,0,.175);
pointer-events: none;
opacity: 0;
transform: scale(.2);
transform-origin: 0 -45px;
transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
&.is-visible {
pointer-events: all;
opacity: 1;
transform: scale(1);
}
.emoji-menu-content {
padding: $gl-padding;
......@@ -90,36 +37,97 @@
height: 300px;
overflow-y: scroll;
h5 {
clear: left;
input.emoji-search{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right 5px center;
background-size: 16px;
}
ul {
list-style-type: none;
margin-left: -20px;
margin-bottom: 20px;
overflow: auto;
}
}
input.emoji-search{
background: image-url("icon-search.png") 240px no-repeat;
}
.emoji-menu-list {
list-style: none;
padding-left: 0;
margin-bottom: 0;
}
.emoji-menu-list-item {
padding: 3px;
margin-left: 1px;
margin-right: 1px;
}
li {
.emoji-menu-btn {
display: block;
cursor: pointer;
width: 30px;
height: 30px;
text-align: center;
float: left;
margin: 3px;
list-decorate: none;
@include border-radius(5px);
padding: 0;
background: none;
border: 0;
border-radius: $border-radius-base;
transition: transform .15s cubic-bezier(.3, 0, .2, 2);
&:hover {
background-color: #ccc;
background-color: transparent;
outline: 0;
transform: scale(1.3);
}
&:focus,
&:active {
outline: 0;
}
.emoji-icon {
display: inline-block;
position: relative;
top: 3px;
}
}
.award-menu-holder {
display: inline-block;
position: relative;
}
.award-control {
margin-right: 5px;
padding-left: 5px;
padding-right: 5px;
line-height: 20px;
outline: 0;
&.active,
&:active {
background-color: $white-dark;
box-shadow: none;
outline: 0;
}
&.is-loading {
.award-control-icon {
display: none;
}
.award-control-icon-loading {
display: block;
}
}
.icon,
.award-control-icon {
float: left;
margin-right: 5px;
font-size: 20px;
}
.award-control-icon-loading {
display: none;
}
.award-control-icon {
color: $award-emoji-new-btn-icon-color;
}
}
......@@ -15,7 +15,7 @@ class GroupsController < Groups::ApplicationController
# Load group projects
before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
before_action :event_filter, only: [:show, :events]
before_action :event_filter, only: [:activity]
layout :determine_layout
......@@ -62,8 +62,10 @@ class GroupsController < Groups::ApplicationController
end
end
def events
def activity
respond_to do |format|
format.html
format.json do
load_events
pager_json("events/_events", @events.count)
......
......@@ -52,7 +52,10 @@ class ProjectsFinder
def all_projects(current_user)
if current_user
[current_user.authorized_projects, public_and_internal_projects]
[
current_user.authorized_projects,
public_and_internal_projects
]
else
[Project.public_only]
end
......
......@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User)
user = user_or_email
else
user = User.find_by(email: user_or_email.try(:downcase))
user = User.find_by_any_email(user_or_email.try(:downcase))
end
if user
......
......@@ -46,9 +46,23 @@ module Ci
acts_as_taggable
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# This method performs a *partial* match on tokens, thus a query for "a"
# will match any runner where the token contains the letter "a". As a result
# you should *not* use this method for non-admin purposes as otherwise users
# might be able to query a list of all runners.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def self.search(query)
where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query',
query: "%#{query.try(:downcase)}%")
t = arel_table
pattern = "%#{query}%"
where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
end
def set_default_values
......
......@@ -61,12 +61,29 @@ module Issuable
end
module ClassMethods
# Searches for records with a matching title.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query)
where("LOWER(title) like :query", query: "%#{query.downcase}%")
where(arel_table[:title].matches("%#{query}%"))
end
# Searches for records with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def full_search(query)
where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%")
t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
def sort(method)
......
......@@ -33,8 +33,18 @@ class Group < Namespace
after_destroy :post_destroy_hook
class << self
# Searches for groups matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query)
where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%")
table = Namespace.arel_table
pattern = "%#{query}%"
where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
end
def sort(method)
......
......@@ -135,7 +135,6 @@ class MergeRequest < ActiveRecord::Base
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
......@@ -161,6 +160,24 @@ class MergeRequest < ActiveRecord::Base
super("merge_requests", /(?<merge_request>\d+)/)
end
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of
# ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries
# using multiple sub-queries especially when combined with an OR statement.
# UNIONs on the other hand perform much better in these cases.
#
# relation - An ActiveRecord::Relation that returns a list of Projects.
#
# Returns an ActiveRecord::Relation.
def self.in_projects(relation)
source = where(source_project_id: relation).select(:id)
target = where(target_project_id: relation).select(:id)
union = Gitlab::SQL::Union.new([source, target])
where("merge_requests.id IN (#{union.to_sql})")
end
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
......
......@@ -58,9 +58,18 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title
class << self
# Searches for milestones matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query)
query = "%#{query}%"
where("title like ? or description like ?", query, query)
t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
end
......
......@@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
end
# Searches for namespaces matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation
def search(query)
where("name LIKE :query OR path LIKE :query", query: "%#{query}%")
t = arel_table
pattern = "%#{query}%"
where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
end
def clean_path(path)
......
......@@ -44,6 +44,7 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
before_validation :set_award!
before_validation :clear_blank_line_code!
validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
......@@ -63,7 +64,7 @@ class Note < ActiveRecord::Base
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) }
scope :not_inline, ->{ where(line_code: nil) }
scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) }
......@@ -105,8 +106,18 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end
# Searches for notes matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search(query)
where("LOWER(note) like :query", query: "%#{query.downcase}%")
table = arel_table
pattern = "%#{query}%"
where(table[:note].matches(pattern))
end
def grouped_awards
......@@ -365,6 +376,10 @@ class Note < ActiveRecord::Base
private
def clear_blank_line_code!
self.line_code = nil if self.line_code.blank?
end
def awards_supported?
(for_issue? || for_merge_request?) && !for_diff_line?
end
......
......@@ -266,13 +266,31 @@ class Project < ActiveRecord::Base
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end
# Searches for a list of projects based on the query given in `query`.
#
# On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
# search. On MySQL a regular "LIKE" is used as it's already
# case-insensitive.
#
# query - The search query as a String.
def search(query)
ptable = arel_table
ntable = Namespace.arel_table
pattern = "%#{query}%"
projects = select(:id).where(
ptable[:path].matches(pattern).
or(ptable[:name].matches(pattern)).
or(ptable[:description].matches(pattern))
)
namespaces = select(:id).
joins(:namespace).
where('LOWER(projects.name) LIKE :query OR
LOWER(projects.path) LIKE :query OR
LOWER(namespaces.name) LIKE :query OR
LOWER(projects.description) LIKE :query',
query: "%#{query.try(:downcase)}%")
where(ntable[:name].matches(pattern))
union = Gitlab::SQL::Union.new([projects, namespaces])
where("projects.id IN (#{union.to_sql})")
end
def search_by_visibility(level)
......@@ -280,7 +298,10 @@ class Project < ActiveRecord::Base
end
def search_by_title(query)
non_archived.where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%")
pattern = "%#{query}%"
table = Project.arel_table
non_archived.where(table[:name].matches(pattern))
end
def find_with_namespace(id)
......@@ -909,13 +930,13 @@ class Project < ActiveRecord::Base
end
def valid_runners_token? token
self.runners_token && self.runners_token == token
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
# TODO (ayufan): For now we use runners_token (backward compatibility)
# In 8.4 every build will have its own individual token valid for time of build
def valid_build_token? token
self.builds_enabled? && self.runners_token && self.runners_token == token
self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
def build_coverage_enabled?
......
......@@ -26,7 +26,7 @@ class CiService < Service
default_value_for :category, 'ci'
def valid_token?(token)
self.respond_to?(:token) && self.token.present? && self.token == token
self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
def supported_events
......
......@@ -113,12 +113,32 @@ class Snippet < ActiveRecord::Base
end
class << self
# Searches for snippets with a matching title or file name.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search(query)
where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%")
t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
end
# Searches for snippets with matching content.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search_code(query)
where('(content LIKE :query)', query: "%#{query}%")
table = Snippet.arel_table
pattern = "%#{query}%"
where(table[:content].matches(pattern))
end
def accessible_to(user)
......
......@@ -286,8 +286,22 @@ class User < ActiveRecord::Base
end
end
# Searches users matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query)
where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%")
table = arel_table
pattern = "%#{query}%"
where(
table[:name].matches(pattern).
or(table[:email].matches(pattern)).
or(table[:username].matches(pattern))
)
end
def by_login(login)
......
......@@ -10,9 +10,8 @@ module Search
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group
project_ids = projects.pluck(:id)
Gitlab::SearchResults.new(project_ids, params[:search])
Gitlab::SearchResults.new(projects, params[:search])
end
end
end
......@@ -7,7 +7,7 @@ module Search
end
def execute
Gitlab::ProjectSearchResults.new(project.id,
Gitlab::ProjectSearchResults.new(project,
params[:search],
params[:repository_ref])
end
......
......@@ -7,8 +7,9 @@ module Search
end
def execute
snippet_ids = Snippet.accessible_to(current_user).pluck(:id)
Gitlab::SnippetSearchResults.new(snippet_ids, params[:search])
snippets = Snippet.accessible_to(current_user)
Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
end
end
......@@ -2,8 +2,10 @@
.emoji-menu-content
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- AwardEmoji.emoji_by_category.each do |category, emojis|
%h5= AwardEmoji::CATEGORIES[category]
%ul
%h5.emoji-menu-title
= AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list
- emojis.each do |emoji|
%li
%li.pull-left.text-center.emoji-menu-list-item
%button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"}
= emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
.hidden-xs
= render "events/event_last_push", event: @last_push
.nav-block
- if current_user
.controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
%i.fa.fa-rss
= render 'shared/event_filter'
.content_list
= spinner
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
- page_title "Activity"
- header_title group_title(@group, "Activity", activity_group_path(@group))
%section.activities
= render 'activities'
......@@ -30,26 +30,13 @@
%ul.nav-links
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
%li
= link_to "#projects", 'data-toggle' => 'tab' do
Projects
- if can?(current_user, :read_group, @group)
%div{ class: container_class }
.tab-content
.tab-pane.active#activity
.activity-filter-block
- if current_user
= render "events/event_last_push", event: @last_push
= render 'shared/event_filter'
.content_list{data: {href: events_group_path}}
= spinner
.tab-pane#projects
.tab-pane.active#projects
= render "projects", projects: @projects
- else
......
......@@ -9,10 +9,15 @@
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do
= icon('dashboard fw')
= icon('group fw')
%span
Group
- if can?(current_user, :read_group, @group)
= nav_link(path: 'groups#activity') do
= link_to activity_group_path(@group), title: 'Activity' do
= icon('dashboard fw')
%span
Activity
- if current_user
= nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do
......
......@@ -71,7 +71,7 @@
.merge-requests
= render 'merge_requests'
.content-block
.content-block.content-block-small
= render 'votes/votes_block', votable: @issue
.row
......
......@@ -68,7 +68,7 @@
.tab-content
#notes.notes.tab-pane.voting_notes
.content-block.oneline-block
.content-block.content-block-small.oneline-block
= render 'votes/votes_block', votable: @merge_request
.row
......
......@@ -6,13 +6,20 @@
- else
Any
%b.caret
%ul.dropdown-menu
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Filter results by group
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-content
%ul
%li
= link_to search_filter_path(group_id: nil) do
= link_to search_filter_path(group_id: nil), class: ("is-active" if !params[:group_id].present?) do
Any
%li.divider
- current_user.authorized_groups.sort_by(&:name).each do |group|
%li
= link_to search_filter_path(group_id: group.id, project_id: nil) do
= link_to search_filter_path(group_id: group.id, project_id: nil), class: ("is-active" if params[:group_id] == group.id.to_s) do
= group.name
.dropdown.inline.prepend-left-10.project-filter
......@@ -23,11 +30,18 @@
- else
Any
%b.caret
%ul.dropdown-menu
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Filter results by project
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-content
%ul
%li
= link_to search_filter_path(project_id: nil) do
= link_to search_filter_path(project_id: nil), class: ("is-active" if !params[:project_id].present?) do
Any
%li.divider
- current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
%li
= link_to search_filter_path(project_id: project.id, group_id: nil) do
= link_to search_filter_path(project_id: project.id, group_id: nil), class: ("is-active" if params[:project_id] == project.id.to_s) do
= project.name_with_namespace
.awards.votes-block
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
.award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)}
%button.btn.award-control.js-emoji-btn.has_tooltip{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user), data: {placement: "top"}}
= emoji_icon(emoji)
.counter
%span.award-control-text.js-counter
= notes.count
- if current_user
.awards-controls
%a.add-award{"href" => "#"}
= icon('smile-o')
%div.award-menu-holder.js-award-holder
%a.btn.award-control.js-add-award{"href" => "#"}
= icon('smile-o', {class: "award-control-icon"})
= icon('spinner spin', {class: "award-control-icon award-control-icon-loading"})
%span.award-control-text
Add
- if current_user
:javascript
......@@ -23,17 +26,3 @@
noteable_id,
aliases
);
$(".awards").on("click", ".emoji-menu-content li", function(e) {
var emoji = $(this).find(".emoji-icon").data("emoji");
awards_handler.addAward(emoji);
});
$(".awards").on("click", ".award", function(e) {
var emoji = $(this).find(".icon").data("emoji");
awards_handler.addAward(emoji);
});
$(".award").tooltip();
$(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false});
......@@ -34,7 +34,7 @@ module Gitlab
config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt, :variables)
config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt, :variables, :import_url)
# Enable escaping HTML in JSON.
config.active_support.escape_html_entities_in_json = true
......
......@@ -203,11 +203,11 @@ Devise.setup do |config|
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
#
# config.warden do |manager|
# manager.failure_app = AnotherApp
config.warden do |manager|
manager.failure_app = Gitlab::DeviseFailure
# manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
end
if Gitlab::LDAP::Config.enabled?
Gitlab.config.ldap.servers.values.each do |server|
......
# This patches ActiveRecord so indexes created using the MySQL adapter ignore
# any PostgreSQL specific options (e.g. `using: :gin`).
#
# These patches do the following for MySQL:
#
# 1. Indexes created using the :opclasses option are ignored (as they serve no
# purpose on MySQL).
# 2. When creating an index with `using: :gin` the `using` option is discarded
# as :gin is not a valid value for MySQL.
# 3. The `:opclasses` option is stripped from add_index_options in case it's
# used anywhere other than in the add_index methods.
if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
module ActiveRecord
module ConnectionAdapters
class Mysql2Adapter < AbstractMysqlAdapter
alias_method :__gitlab_add_index, :add_index
alias_method :__gitlab_add_index_sql, :add_index_sql
alias_method :__gitlab_add_index_options, :add_index_options
def add_index(table_name, column_name, options = {})
unless options[:opclasses]
__gitlab_add_index(table_name, column_name, options)
end
end
def add_index_sql(table_name, column_name, options = {})
unless options[:opclasses]
__gitlab_add_index_sql(table_name, column_name, options)
end
end
def add_index_options(table_name, column_name, options = {})
if options[:using] and options[:using] == :gin
options = options.dup
options.delete(:using)
end
if options[:opclasses]
options = options.dup
options.delete(:opclasses)
end
__gitlab_add_index_options(table_name, column_name, options)
end
end
end
end
end
# rubocop:disable all
# These changes add support for PostgreSQL operator classes when creating
# indexes and dumping/loading schemas. Taken from Rails pull request
# https://github.com/rails/rails/pull/19090.
#
# License:
#
# Copyright (c) 2004-2016 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require 'date'
require 'set'
require 'bigdecimal'
require 'bigdecimal/util'
# As the Struct definition is changed in this PR/patch we have to first remove
# the existing one.
ActiveRecord::ConnectionAdapters.send(:remove_const, :IndexDefinition)
module ActiveRecord
module ConnectionAdapters #:nodoc:
# Abstract representation of an index definition on a table. Instances of
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclasses) #:nodoc:
end
end
end
module ActiveRecord
module ConnectionAdapters # :nodoc:
module SchemaStatements
def add_index_options(table_name, column_name, options = {}) #:nodoc:
column_names = Array(column_name)
index_name = index_name(table_name, column: column_names)
options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclasses)
index_type = options[:unique] ? "UNIQUE" : ""
index_type = options[:type].to_s if options.key?(:type)
index_name = options[:name].to_s if options.key?(:name)
max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
if options.key?(:algorithm)
algorithm = index_algorithms.fetch(options[:algorithm]) {
raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
}
end
using = "USING #{options[:using]}" if options[:using].present?
if supports_partial_index?
index_options = options[:where] ? " WHERE #{options[:where]}" : ""
end
if index_name.length > max_index_length
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
end
if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")
[index_name, index_type, index_columns, index_options, algorithm, using]
end
end
end
end
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module SchemaStatements
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil)
result = query(<<-SQL, 'SCHEMA')
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
WHERE i.relkind = 'i'
AND d.indisprimary = 'f'
AND t.relname = '#{table_name}'
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
ORDER BY i.relname
SQL
result.map do |row|
index_name = row[0]
unique = row[1] == 't'
indkey = row[2].split(" ")
inddef = row[3]
oid = row[4]
columns = Hash[query(<<-SQL, "SCHEMA")]
SELECT a.attnum, a.attname
FROM pg_attribute a
WHERE a.attrelid = #{oid}
AND a.attnum IN (#{indkey.join(",")})
SQL
column_names = columns.values_at(*indkey).compact
unless column_names.empty?
# add info on sort order for columns (only desc order is explicitly specified, asc is the default)
desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
where = inddef.scan(/WHERE (.+)$/).flatten[0]
using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
opclasses = Hash[inddef.scan(/\((.+)\)$/).flatten[0].split(',').map do |column_and_opclass|
column, opclass = column_and_opclass.split(' ').map(&:strip)
[column, opclass] if opclass
end.compact]
IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using, opclasses)
end
end.compact
end
def add_index(table_name, column_name, options = {}) #:nodoc:
index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}"
end
protected
def quoted_columns_for_index(column_names, options = {})
column_opclasses = options[:opclasses] || {}
column_names.map {|name| "#{quote_column_name(name)} #{column_opclasses[name]}"}
end
end
end
end
end
module ActiveRecord
class SchemaDumper
private
def indexes(table, stream)
if (indexes = @connection.indexes(table)).any?
add_index_statements = indexes.map do |index|
statement_parts = [
"add_index #{remove_prefix_and_suffix(index.table).inspect}",
index.columns.inspect,
"name: #{index.name.inspect}",
]
statement_parts << 'unique: true' if index.unique
index_lengths = (index.lengths || []).compact
statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any?
index_orders = index.orders || {}
statement_parts << "order: #{index.orders.inspect}" if index_orders.any?
statement_parts << "where: #{index.where.inspect}" if index.where
statement_parts << "using: #{index.using.inspect}" if index.using
statement_parts << "type: #{index.type.inspect}" if index.type
statement_parts << "opclasses: #{index.opclasses}" if index.opclasses.present?
" #{statement_parts.join(', ')}"
end
stream.puts add_index_statements.sort.join("\n")
stream.puts
end
end
end
end
......@@ -382,7 +382,7 @@ Rails.application.routes.draw do
get :issues
get :merge_requests
get :projects
get :events
get :activity
end
scope module: :groups do
......
class AddTrigramIndexesForSearching < ActiveRecord::Migration
disable_ddl_transaction!
def up
return unless Gitlab::Database.postgresql?
unless trigrams_enabled?
raise 'You must enable the pg_trgm extension. You can do so by running ' \
'"CREATE EXTENSION pg_trgm;" as a PostgreSQL super user, this must be ' \
'done for every GitLab database. For more information see ' \
'http://www.postgresql.org/docs/current/static/sql-createextension.html'
end
# trigram indexes are case-insensitive so we can just index the column
# instead of indexing lower(column)
to_index.each do |table, columns|
columns.each do |column|
execute "CREATE INDEX CONCURRENTLY index_#{table}_on_#{column}_trigram ON #{table} USING gin(#{column} gin_trgm_ops);"
end
end
end
def down
return unless Gitlab::Database.postgresql?
to_index.each do |table, columns|
columns.each do |column|
remove_index table, name: "index_#{table}_on_#{column}_trigram"
end
end
end
def trigrams_enabled?
res = execute("SELECT true AS enabled FROM pg_available_extensions WHERE name = 'pg_trgm' AND installed_version IS NOT NULL;")
row = res.first
row && row['enabled'] == 't' ? true : false
end
def to_index
{
ci_runners: [:token, :description],
issues: [:title, :description],
merge_requests: [:title, :description],
milestones: [:title, :description],
namespaces: [:name, :path],
notes: [:note],
projects: [:name, :path, :description],
snippets: [:title, :file_name],
users: [:username, :name, :email]
}
end
end
class DisallowBlankLineCodeOnNote < ActiveRecord::Migration
def up
execute("UPDATE notes SET line_code = NULL WHERE line_code = ''")
end
def down
# noop
end
end
......@@ -15,6 +15,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_trgm"
create_table "abuse_reports", force: :cascade do |t|
t.integer "reporter_id"
......@@ -258,6 +259,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do
t.string "architecture"
end
add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"}
create_table "ci_services", force: :cascade do |t|
t.string "type"
t.string "title"
......@@ -417,11 +421,13 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "keys", force: :cascade do |t|
t.integer "user_id"
......@@ -543,12 +549,14 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree
add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "milestones", force: :cascade do |t|
t.string "title", null: false
......@@ -562,10 +570,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do
end
add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree
add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree
add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "namespaces", force: :cascade do |t|
t.string "name", null: false
......@@ -580,8 +590,10 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: :cascade do |t|
......@@ -607,6 +619,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree
add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
......@@ -705,9 +718,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
......@@ -785,7 +801,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree
add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"}
add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
......@@ -919,9 +937,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree
add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
add_index "users", ["name"], name: "index_users_on_name", using: :btree
add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
add_index "users", ["username"], name: "index_users_on_username", using: :btree
add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
create_table "users_star_projects", force: :cascade do |t|
t.integer "project_id", null: false
......
......@@ -64,7 +64,7 @@ Save the file and restart GitLab: `sudo service gitlab restart`.
For Omnibus installations, edit `/etc/gitlab/gitlab.rb` and add the line:
```
gitlab-rails['gitlab_default_projects_features_builds'] = false
gitlab_rails['gitlab_default_projects_features_builds'] = false
```
Save the file and reconfigure GitLab: `sudo gitlab-ctl reconfigure`.
# Development
- [Architecture](architecture.md) of GitLab
- [Benchmarking](benchmarking.md)
- [CI setup](ci_setup.md) for testing GitLab
- [Gotchas](gotchas.md) to avoid
- [How to dump production data to staging](db_dump.md)
......
# Benchmarking
GitLab CE comes with a set of benchmarks that are executed for every build. This
makes it easier to measure performance of certain components over time.
Benchmarks are written as RSpec tests using a few extra helpers. To write a
benchmark, first tag the top-level `describe`:
```ruby
describe MaruTheCat, benchmark: true do
end
```
This ensures the benchmark is executed separately from other test collections.
It also exposes the various RSpec matchers used for writing benchmarks to the
test group.
Next, lets write the actual benchmark:
```ruby
describe MaruTheCat, benchmark: true do
let(:maru) { MaruTheChat.new }
describe '#jump_in_box' do
benchmark_subject { maru.jump_in_box }
it { is_expected.to iterate_per_second(9000) }
end
end
```
Here `benchmark_subject` is a small wrapper around RSpec's `subject` method that
makes it easier to specify the subject of a benchmark. Using RSpec's regular
`subject` would require us to write the following instead:
```ruby
subject { -> { maru.jump_in_box } }
```
The `iterate_per_second` matcher defines the amount of times per second a
subject should be executed. The higher the amount of iterations the better.
By default the allowed standard deviation is a maximum of 30%. This can be
adjusted by chaining the `with_maximum_stddev` on the `iterate_per_second`
matcher:
```ruby
it { is_expected.to iterate_per_second(9000).with_maximum_stddev(50) }
```
This can be useful if the code in question depends on external resources of
which the performance can vary a lot (e.g. physical HDDs, network calls, etc).
However, in most cases 30% should be enough so only change this when really
needed.
## Benchmarks Location
Benchmarks should be stored in `spec/benchmarks` and should follow the regular
Rails specs structure. That is, model benchmarks go in `spec/benchmark/models`,
benchmarks for code in the `lib` directory go in `spec/benchmarks/lib`, etc.
## Underlying Technology
The benchmark setup uses [benchmark-ips][benchmark-ips] which takes care of the
heavy lifting such as warming up code, calculating iterations, standard
deviation, etc.
[benchmark-ips]: https://github.com/evanphx/benchmark-ips
......@@ -97,6 +97,17 @@ To change the Unicorn workers when you have the Omnibus package please see [the
If you want to run the database separately expect a size of about 1 MB per user.
### PostgreSQL Requirements
Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
GitLab database. This extension can be enabled (using a PostgreSQL super user)
by running the following query for every database:
CREATE EXTENSION pg_trgm;
On some systems you may need to install an additional package (e.g.
`postgresql-contrib`) for this extension to become available.
## Redis and Sidekiq
Redis stores all user sessions and the background task queue.
......
......@@ -131,6 +131,58 @@ On the sign in page there should now be a SAML button below the regular sign in
Click the icon to begin the authentication process. If everything goes well the user
will be returned to GitLab and will be signed in.
## Customization
### `attribute_statements`
>**Note:**
This setting is only available on GitLab 8.6 and above.
This setting should only be used to map attributes that are part of the
OmniAuth info hash schema.
`attribute_statements` is used to map Attribute Names in a SAMLResponse to entries
in the OmniAuth [info hash](https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema#schema-10-and-later).
For example, if your SAMLResponse contains an Attribute called 'EmailAddress',
specify `{ email: ['EmailAddress'] }` to map the Attribute to the
corresponding key in the info hash. URI-named Attributes are also supported, e.g.
`{ email: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] }`.
This setting allows you tell GitLab where to look for certain attributes required
to create an account. Like mentioned above, if your IdP sends the user's email
address as `EmailAddress` instead of `email`, let GitLab know by setting it on
your configuration:
```yaml
args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
attribute_statements: { email: ['EmailAddress'] }
}
```
### `allowed_clock_drift`
The clock of the Identity Provider may drift slightly ahead of your system clocks.
To allow for a small amount of clock drift you can use `allowed_clock_drift` within
your settings. Its value must be given in a number (and/or fraction) of seconds.
The value given is added to the current time at which the response is validated.
```yaml
args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
attribute_statements: { email: ['EmailAddress'] },
allowed_clock_drift: 1 # for one second clock drift
}
```
## Troubleshooting
### 500 error after login
......
......@@ -15,6 +15,10 @@ Feature: Groups
Scenario: I should see group "Owned" dashboard list
When I visit group "Owned" page
Then I should see group "Owned" projects list
@javascript
Scenario: I should see group "Owned" activity feed
When I visit group "Owned" activity page
And I should see projects activity feed
Scenario: I should see group "Owned" issues list
......
......@@ -10,7 +10,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I click the thumbsup award Emoji' do
page.within '.awards' do
thumbsup = page.find('.award .emoji-1F44D')
thumbsup = page.first('.award-control')
thumbsup.click
thumbsup.hover
sleep 0.3
......@@ -18,23 +18,23 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'I click to emoji-picker' do
page.within '.awards-controls' do
page.find('.add-award').click
page.within '.awards' do
page.find('.js-add-award').click
end
end
step 'I click to emoji in the picker' do
page.within '.emoji-menu-content' do
page.first('.emoji-icon').click
page.first('.js-emoji-btn').click
end
end
step 'I can remove it by clicking to icon' do
page.within '.awards' do
expect do
page.find('.award.active').click
page.find('.js-emoji-btn.active').click
sleep 0.3
end.to change{ page.all(".award").size }.from(3).to(2)
end.to change{ page.all(".award-control.js-emoji-btn").size }.from(3).to(2)
end
end
......@@ -49,23 +49,23 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
sleep 0.2
page.within '.awards' do
expect(page).to have_selector '.award'
expect(page.find('.award.active .counter')).to have_content '1'
expect(page.find('.award.active')['data-original-title']).to eq('me')
expect(page).to have_selector '.js-emoji-btn'
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
expect(page.find('.js-emoji-btn.active')['data-original-title']).to eq('me')
end
end
step 'I have no awards added' do
page.within '.awards' do
expect(page).to have_selector '.award'
expect(page.all('.award').size).to eq(2)
expect(page).to have_selector '.award-control.js-emoji-btn'
expect(page.all('.award-control.js-emoji-btn').size).to eq(2)
# Check tooltip data
page.all('.award').each do |element|
page.all('.award-control.js-emoji-btn').each do |element|
expect(element['title']).to eq("")
end
page.all('.award .counter').each do |element|
page.all('.award-control .js-counter').each do |element|
expect(element).to have_content '0'
end
end
......
......@@ -27,6 +27,10 @@ module SharedPaths
visit group_path(Group.find_by(name: "Owned"))
end
step 'I visit group "Owned" activity page' do
visit activity_group_path(Group.find_by(name: "Owned"))
end
step 'I visit group "Owned" issues page' do
visit issues_group_path(Group.find_by(name: "Owned"))
end
......
module Gitlab
class DeviseFailure < Devise::FailureApp
protected
# Override `Devise::FailureApp#request_format` to handle a special case
#
# This tells Devise to handle an unauthenticated `.zip` request as an HTML
# request (i.e., redirect to sign in).
#
# Otherwise, Devise would respond with a 401 Unauthorized with
# `Content-Type: application/zip` and a response body in plaintext, and the
# browser would freak out.
#
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/12944
def request_format
if request.format == :zip
Mime::Type.lookup_by_extension(:html).ref
else
super
end
end
end
end
......@@ -45,12 +45,15 @@ module Gitlab
direction: :asc).each do |raw_data|
pull_request = PullRequestFormatter.new(project, raw_data)
if !pull_request.cross_project? && pull_request.valid?
merge_request = MergeRequest.create!(pull_request.attributes)
if pull_request.valid?
merge_request = MergeRequest.new(pull_request.attributes)
if merge_request.save
import_comments(pull_request.number, merge_request)
import_comments_on_diff(pull_request.number, merge_request)
end
end
end
true
rescue ActiveRecord::RecordInvalid => e
......
......@@ -17,16 +17,12 @@ module Gitlab
}
end
def cross_project?
source_repo.id != target_repo.id
end
def number
raw_data.number
end
def valid?
source_branch.present? && target_branch.present?
!cross_project? && source_branch.present? && target_branch.present?
end
private
......@@ -53,6 +49,10 @@ module Gitlab
raw_data.body || ""
end
def cross_project?
source_repo.present? && target_repo.present? && source_repo.id != target_repo.id
end
def description
formatter.author_line(author) + body
end
......
......@@ -2,8 +2,8 @@ module Gitlab
class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref
def initialize(project_id, query, repository_ref = nil)
@project = Project.find(project_id)
def initialize(project, query, repository_ref = nil)
@project = project
@repository_ref = if repository_ref.present?
repository_ref
else
......@@ -73,7 +73,7 @@ module Gitlab
end
def notes
Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC')
project.notes.user.search(query).order('updated_at DESC')
end
def commits
......@@ -84,8 +84,8 @@ module Gitlab
end
end
def limit_project_ids
[project.id]
def project_ids_relation
project
end
end
end
......@@ -2,12 +2,12 @@ module Gitlab
class SearchResults
attr_reader :query
# Limit search results by passed project ids
# Limit search results by passed projects
# It allows us to search only for projects user has access to
attr_reader :limit_project_ids
attr_reader :limit_projects
def initialize(limit_project_ids, query)
@limit_project_ids = limit_project_ids || Project.all
def initialize(limit_projects, query)
@limit_projects = limit_projects || Project.all
@query = Shellwords.shellescape(query) if query.present?
end
......@@ -27,7 +27,8 @@ module Gitlab
end
def total_count
@total_count ||= projects_count + issues_count + merge_requests_count + milestones_count
@total_count ||= projects_count + issues_count + merge_requests_count +
milestones_count
end
def projects_count
......@@ -53,27 +54,29 @@ module Gitlab
private
def projects
Project.where(id: limit_project_ids).search(query)
limit_projects.search(query)
end
def issues
issues = Issue.where(project_id: limit_project_ids)
issues = Issue.where(project_id: project_ids_relation)
if query =~ /#(\d+)\z/
issues = issues.where(iid: $1)
else
issues = issues.full_search(query)
end
issues.order('updated_at DESC')
end
def milestones
milestones = Milestone.where(project_id: limit_project_ids)
milestones = Milestone.where(project_id: project_ids_relation)
milestones = milestones.search(query)
milestones.order('updated_at DESC')
end
def merge_requests
merge_requests = MergeRequest.in_projects(limit_project_ids)
merge_requests = MergeRequest.in_projects(project_ids_relation)
if query =~ /[#!](\d+)\z/
merge_requests = merge_requests.where(iid: $1)
else
......@@ -89,5 +92,9 @@ module Gitlab
def per_page
20
end
def project_ids_relation
limit_projects.select(:id).reorder(nil)
end
end
end
......@@ -2,10 +2,10 @@ module Gitlab
class SnippetSearchResults < SearchResults
include SnippetsHelper
attr_reader :limit_snippet_ids
attr_reader :limit_snippets
def initialize(limit_snippet_ids, query)
@limit_snippet_ids = limit_snippet_ids
def initialize(limit_snippets, query)
@limit_snippets = limit_snippets
@query = query
end
......@@ -35,11 +35,11 @@ module Gitlab
private
def snippet_titles
Snippet.where(id: limit_snippet_ids).search(query).order('updated_at DESC')
limit_snippets.search(query).order('updated_at DESC')
end
def snippet_blobs
Snippet.where(id: limit_snippet_ids).search_code(query).order('updated_at DESC')
limit_snippets.search_code(query).order('updated_at DESC')
end
def default_scope
......
......@@ -46,20 +46,11 @@ namespace :spec do
run_commands(cmds)
end
desc 'GitLab | Rspec | Run benchmark specs'
task :benchmark do
cmds = [
%W(rake gitlab:setup),
%W(rspec spec --tag @benchmark)
]
run_commands(cmds)
end
desc 'GitLab | Rspec | Run other specs'
task :other do
cmds = [
%W(rake gitlab:setup),
%W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services --tag ~@benchmark)
%W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services)
]
run_commands(cmds)
end
......@@ -69,7 +60,7 @@ desc "GitLab | Run specs"
task :spec do
cmds = [
%W(rake gitlab:setup),
%W(rspec spec --tag ~@benchmark),
%W(rspec spec),
]
run_commands(cmds)
end
......
require 'spec_helper'
describe IssuesFinder, benchmark: true do
describe '#execute' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:label1) { create(:label, project: project, title: 'A') }
let(:label2) { create(:label, project: project, title: 'B') }
before do
10.times do |n|
issue = create(:issue, author: user, project: project)
if n > 4
create(:label_link, label: label1, target: issue)
create(:label_link, label: label2, target: issue)
end
end
end
describe 'retrieving issues without labels' do
let(:finder) do
IssuesFinder.new(user, scope: 'all', label_name: Label::None.title,
state: 'opened')
end
benchmark_subject { finder.execute }
it { is_expected.to iterate_per_second(2000) }
end
describe 'retrieving issues with labels' do
let(:finder) do
IssuesFinder.new(user, scope: 'all', label_name: label1.title,
state: 'opened')
end
benchmark_subject { finder.execute }
it { is_expected.to iterate_per_second(1000) }
end
describe 'retrieving issues for a single project' do
let(:finder) do
IssuesFinder.new(user, scope: 'all', label_name: Label::None.title,
state: 'opened', project_id: project.id)
end
benchmark_subject { finder.execute }
it { is_expected.to iterate_per_second(2000) }
end
end
end
require 'spec_helper'
describe TrendingProjectsFinder, benchmark: true do
describe '#execute' do
let(:finder) { described_class.new }
let(:user) { create(:user) }
# to_a is used to force actually running the query (instead of just building
# it).
benchmark_subject { finder.execute(user).non_archived.to_a }
it { is_expected.to iterate_per_second(500) }
end
end
require 'spec_helper'
describe Banzai::Filter::ReferenceFilter, benchmark: true do
let(:input) do
html = <<-EOF
<p>Hello @alice and @bob, how are you doing today?</p>
<p>This is simple @dummy text to see how the @ReferenceFilter class performs
when @processing HTML.</p>
EOF
Nokogiri::HTML.fragment(html)
end
let(:project) { create(:empty_project) }
let(:filter) { described_class.new(input, project: project) }
describe '#replace_text_nodes_matching' do
let(:iterations) { 6000 }
describe 'with identical input and output HTML' do
benchmark_subject do
filter.replace_text_nodes_matching(User.reference_pattern) do |content|
content
end
end
it { is_expected.to iterate_per_second(iterations) }
end
describe 'with different input and output HTML' do
benchmark_subject do
filter.replace_text_nodes_matching(User.reference_pattern) do |content|
'@eve'
end
end
it { is_expected.to iterate_per_second(iterations) }
end
end
end
require 'spec_helper'
describe Milestone, benchmark: true do
describe '#sort_issues' do
let(:milestone) { create(:milestone) }
let(:issue1) { create(:issue, milestone: milestone) }
let(:issue2) { create(:issue, milestone: milestone) }
let(:issue3) { create(:issue, milestone: milestone) }
let(:issue_ids) { [issue3.id, issue2.id, issue1.id] }
benchmark_subject { milestone.sort_issues(issue_ids) }
it { is_expected.to iterate_per_second(500) }
end
end
require 'spec_helper'
describe Project, benchmark: true do
describe '.trending' do
let(:group) { create(:group) }
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :public, group: group) }
let(:iterations) { 500 }
before do
2.times do
create(:note_on_commit, project: project1)
end
create(:note_on_commit, project: project2)
end
describe 'without an explicit start date' do
benchmark_subject { described_class.trending.to_a }
it { is_expected.to iterate_per_second(iterations) }
end
describe 'with an explicit start date' do
let(:date) { 1.month.ago }
benchmark_subject { described_class.trending(date).to_a }
it { is_expected.to iterate_per_second(iterations) }
end
end
describe '.find_with_namespace' do
let(:group) { create(:group, name: 'sisinmaru') }
let(:project) { create(:project, name: 'maru', namespace: group) }
describe 'using a capitalized namespace' do
benchmark_subject { described_class.find_with_namespace('sisinmaru/MARU') }
it { is_expected.to iterate_per_second(600) }
end
describe 'using a lowercased namespace' do
benchmark_subject { described_class.find_with_namespace('sisinmaru/maru') }
it { is_expected.to iterate_per_second(600) }
end
end
end
require 'spec_helper'
describe ProjectTeam, benchmark: true do
describe '#max_member_access' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, group: group) }
let(:user) { create(:user) }
before do
project.team << [user, :master]
5.times do
project.team << [create(:user), :reporter]
project.group.add_user(create(:user), :reporter)
end
end
benchmark_subject { project.team.max_member_access(user.id) }
it { is_expected.to iterate_per_second(35000) }
end
end
require 'spec_helper'
describe User, benchmark: true do
describe '.all' do
before do
10.times { create(:user) }
end
benchmark_subject { User.all.to_a }
it { is_expected.to iterate_per_second(500) }
end
describe '.by_login' do
before do
%w{Alice Bob Eve}.each do |name|
create(:user,
email: "#{name}@gitlab.com",
username: name,
name: name)
end
end
# The iteration count is based on the query taking little over 1 ms when
# using PostgreSQL.
let(:iterations) { 900 }
describe 'using a capitalized username' do
benchmark_subject { User.by_login('Alice') }
it { is_expected.to iterate_per_second(iterations) }
end
describe 'using a lowercase username' do
benchmark_subject { User.by_login('alice') }
it { is_expected.to iterate_per_second(iterations) }
end
describe 'using a capitalized Email address' do
benchmark_subject { User.by_login('Alice@gitlab.com') }
it { is_expected.to iterate_per_second(iterations) }
end
describe 'using a lowercase Email address' do
benchmark_subject { User.by_login('alice@gitlab.com') }
it { is_expected.to iterate_per_second(iterations) }
end
end
describe '.find_by_any_email' do
let(:user) { create(:user) }
describe 'using a user with only a single Email address' do
let(:email) { user.email }
benchmark_subject { User.find_by_any_email(email) }
it { is_expected.to iterate_per_second(1000) }
end
describe 'using a user with multiple Email addresses' do
let(:email) { user.emails.first.email }
benchmark_subject { User.find_by_any_email(email) }
before do
10.times do
user.emails.create(email: FFaker::Internet.email)
end
end
it { is_expected.to iterate_per_second(1000) }
end
end
end
require 'spec_helper'
describe Projects::CreateService, benchmark: true do
describe '#execute' do
let(:user) { create(:user, :admin) }
let(:group) do
group = create(:group)
create(:group_member, group: group, user: user)
group
end
benchmark_subject do
name = SecureRandom.hex
service = described_class.new(user,
name: name,
path: name,
namespace_id: group.id,
visibility_level: Gitlab::VisibilityLevel::PUBLIC)
service.execute
end
it { is_expected.to iterate_per_second(0.5) }
end
end
......@@ -2,14 +2,24 @@ require "spec_helper"
describe Projects::RepositoriesController do
let(:project) { create(:project) }
let(:user) { create(:user) }
describe "GET archive" do
context 'as a guest' do
it 'responds with redirect in correct format' do
get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip"
expect(response.content_type).to start_with 'text/html'
expect(response).to be_redirect
end
end
context 'as a user' do
let(:user) { create(:user) }
before do
sign_in(user)
project.team << [user, :developer]
sign_in(user)
end
it "uses Gitlab::Workhorse" do
expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip")
......@@ -29,4 +39,5 @@ describe Projects::RepositoriesController do
end
end
end
end
end
......@@ -127,34 +127,6 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
describe '#cross_project?' do
context 'when source, and target repositories are the same' do
let(:raw_data) { OpenStruct.new(base_data) }
it 'returns false' do
expect(pull_request.cross_project?).to eq false
end
end
context 'when source repo is a fork' do
let(:source_repo) { OpenStruct.new(id: 2, fork: true) }
let(:raw_data) { OpenStruct.new(base_data) }
it 'returns true' do
expect(pull_request.cross_project?).to eq true
end
end
context 'when target repo is a fork' do
let(:target_repo) { OpenStruct.new(id: 2, fork: true) }
let(:raw_data) { OpenStruct.new(base_data) }
it 'returns true' do
expect(pull_request.cross_project?).to eq true
end
end
end
describe '#number' do
let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) }
......@@ -166,7 +138,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
describe '#valid?' do
let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') }
context 'when source and target branches exists' do
context 'when source, and target repositories are the same' do
context 'and source and target branches exists' do
let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) }
it 'returns true' do
......@@ -174,7 +147,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
context 'when source branch doesn not exists' do
context 'and source branch doesn not exists' do
let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) }
it 'returns false' do
......@@ -182,7 +155,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
context 'when target branch doesn not exists' do
context 'and target branch doesn not exists' do
let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) }
it 'returns false' do
......@@ -190,4 +163,23 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
end
context 'when source repo is a fork' do
let(:source_repo) { OpenStruct.new(id: 2, fork: true) }
let(:raw_data) { OpenStruct.new(base_data) }
it 'returns false' do
expect(pull_request.valid?).to eq false
end
end
context 'when target repo is a fork' do
let(:target_repo) { OpenStruct.new(id: 2, fork: true) }
let(:raw_data) { OpenStruct.new(base_data) }
it 'returns false' do
expect(pull_request.valid?).to eq false
end
end
end
end
......@@ -5,7 +5,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
let(:query) { 'hello world' }
describe 'initialize with empty ref' do
let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, '') }
let(:results) { Gitlab::ProjectSearchResults.new(project, query, '') }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to be_nil }
......@@ -14,7 +14,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' }
let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, ref) }
let(:results) { Gitlab::ProjectSearchResults.new(project, query, ref) }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
......
require 'spec_helper'
describe Gitlab::SearchResults do
let!(:project) { create(:project, name: 'foo') }
let!(:issue) { create(:issue, project: project, title: 'foo') }
let!(:merge_request) do
create(:merge_request, source_project: project, title: 'foo')
end
let!(:milestone) { create(:milestone, project: project, title: 'foo') }
let(:results) { described_class.new(Project.all, 'foo') }
describe '#total_count' do
it 'returns the total amount of search hits' do
expect(results.total_count).to eq(4)
end
end
describe '#projects_count' do
it 'returns the total amount of projects' do
expect(results.projects_count).to eq(1)
end
end
describe '#issues_count' do
it 'returns the total amount of issues' do
expect(results.issues_count).to eq(1)
end
end
describe '#merge_requests_count' do
it 'returns the total amount of merge requests' do
expect(results.merge_requests_count).to eq(1)
end
end
describe '#milestones_count' do
it 'returns the total amount of milestones' do
expect(results.milestones_count).to eq(1)
end
end
describe '#empty?' do
it 'returns true when there are no search results' do
allow(results).to receive(:total_count).and_return(0)
expect(results.empty?).to eq(true)
end
it 'returns false when there are search results' do
expect(results.empty?).to eq(false)
end
end
end
require 'spec_helper'
describe Gitlab::SnippetSearchResults do
let!(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
let(:results) { described_class.new(Snippet.all, 'foo') }
describe '#total_count' do
it 'returns the total amount of search hits' do
expect(results.total_count).to eq(2)
end
end
describe '#snippet_titles_count' do
it 'returns the amount of matched snippet titles' do
expect(results.snippet_titles_count).to eq(1)
end
end
describe '#snippet_blobs_count' do
it 'returns the amount of matched snippet blobs' do
expect(results.snippet_blobs_count).to eq(1)
end
end
end
......@@ -132,4 +132,32 @@ describe Ci::Runner, models: true do
expect(runner.belongs_to_one_project?).to be_truthy
end
end
describe '#search' do
let(:runner) { create(:ci_runner, token: '123abc') }
it 'returns runners with a matching token' do
expect(described_class.search(runner.token)).to eq([runner])
end
it 'returns runners with a partially matching token' do
expect(described_class.search(runner.token[0..2])).to eq([runner])
end
it 'returns runners with a matching token regardless of the casing' do
expect(described_class.search(runner.token.upcase)).to eq([runner])
end
it 'returns runners with a matching description' do
expect(described_class.search(runner.description)).to eq([runner])
end
it 'returns runners with a partially matching description' do
expect(described_class.search(runner.description[0..2])).to eq([runner])
end
it 'returns runners with a matching description regardless of the casing' do
expect(described_class.search(runner.description.upcase)).to eq([runner])
end
end
end
......@@ -32,9 +32,54 @@ describe Issue, "Issuable" do
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
it "matches by title" do
it 'returns notes with a matching title' do
expect(described_class.search(searchable_issue.title)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching title' do
expect(described_class.search('able')).to eq([searchable_issue])
end
it 'returns notes with a matching title regardless of the casing' do
expect(described_class.search(searchable_issue.title.upcase)).
to eq([searchable_issue])
end
end
describe ".full_search" do
let!(:searchable_issue) do
create(:issue, title: "Searchable issue", description: 'kittens')
end
it 'returns notes with a matching title' do
expect(described_class.full_search(searchable_issue.title)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching title' do
expect(described_class.full_search('able')).to eq([searchable_issue])
end
it 'returns notes with a matching title regardless of the casing' do
expect(described_class.full_search(searchable_issue.title.upcase)).
to eq([searchable_issue])
end
it 'returns notes with a matching description' do
expect(described_class.full_search(searchable_issue.description)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching description' do
expect(described_class.full_search(searchable_issue.description)).
to eq([searchable_issue])
end
it 'returns notes with a matching description regardless of the casing' do
expect(described_class.full_search(searchable_issue.description.upcase)).
to eq([searchable_issue])
end
end
describe "#today?" do
......
......@@ -103,4 +103,30 @@ describe Group, models: true do
expect(group.avatar_type).to eq(["only images allowed"])
end
end
describe '.search' do
it 'returns groups with a matching name' do
expect(described_class.search(group.name)).to eq([group])
end
it 'returns groups with a partially matching name' do
expect(described_class.search(group.name[0..2])).to eq([group])
end
it 'returns groups with a matching name regardless of the casing' do
expect(described_class.search(group.name.upcase)).to eq([group])
end
it 'returns groups with a matching path' do
expect(described_class.search(group.path)).to eq([group])
end
it 'returns groups with a partially matching path' do
expect(described_class.search(group.path[0..2])).to eq([group])
end
it 'returns groups with a matching path regardless of the casing' do
expect(described_class.search(group.path.upcase)).to eq([group])
end
end
end
......@@ -80,6 +80,12 @@ describe MergeRequest, models: true do
it { is_expected.to respond_to(:merge_when_build_succeeds) }
end
describe '.in_projects' do
it 'returns the merge requests for a set of projects' do
expect(described_class.in_projects(Project.all)).to eq([subject])
end
end
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "!#{subject.iid}"
......
......@@ -181,4 +181,34 @@ describe Milestone, models: true do
expect(issue4.position).to eq(42)
end
end
describe '.search' do
let(:milestone) { create(:milestone, title: 'foo', description: 'bar') }
it 'returns milestones with a matching title' do
expect(described_class.search(milestone.title)).to eq([milestone])
end
it 'returns milestones with a partially matching title' do
expect(described_class.search(milestone.title[0..2])).to eq([milestone])
end
it 'returns milestones with a matching title regardless of the casing' do
expect(described_class.search(milestone.title.upcase)).to eq([milestone])
end
it 'returns milestones with a matching description' do
expect(described_class.search(milestone.description)).to eq([milestone])
end
it 'returns milestones with a partially matching description' do
expect(described_class.search(milestone.description[0..2])).
to eq([milestone])
end
it 'returns milestones with a matching description regardless of the casing' do
expect(described_class.search(milestone.description.upcase)).
to eq([milestone])
end
end
end
......@@ -41,13 +41,32 @@ describe Namespace, models: true do
it { expect(namespace.human_name).to eq(namespace.owner_name) }
end
describe :search do
before do
@namespace = create :namespace
describe '.search' do
let(:namespace) { create(:namespace) }
it 'returns namespaces with a matching name' do
expect(described_class.search(namespace.name)).to eq([namespace])
end
it 'returns namespaces with a partially matching name' do
expect(described_class.search(namespace.name[0..2])).to eq([namespace])
end
it 'returns namespaces with a matching name regardless of the casing' do
expect(described_class.search(namespace.name.upcase)).to eq([namespace])
end
it 'returns namespaces with a matching path' do
expect(described_class.search(namespace.path)).to eq([namespace])
end
it { expect(Namespace.search(@namespace.path)).to eq([@namespace]) }
it { expect(Namespace.search('unknown')).to eq([]) }
it 'returns namespaces with a partially matching path' do
expect(described_class.search(namespace.path[0..2])).to eq([namespace])
end
it 'returns namespaces with a matching path regardless of the casing' do
expect(described_class.search(namespace.path.upcase)).to eq([namespace])
end
end
describe :move_dir do
......
......@@ -140,10 +140,16 @@ describe Note, models: true do
end
end
describe :search do
let!(:note) { create(:note, note: "WoW") }
describe '.search' do
let(:note) { create(:note, note: 'WoW') }
it { expect(Note.search('wow')).to include(note) }
it 'returns notes with matching content' do
expect(described_class.search(note.note)).to eq([note])
end
it 'returns notes with matching content regardless of the casing' do
expect(described_class.search('WOW')).to eq([note])
end
end
describe :grouped_awards do
......@@ -220,4 +226,12 @@ describe Note, models: true do
expect(note.is_award?).to be_falsy
end
end
describe 'clear_blank_line_code!' do
it 'clears a blank line code before validation' do
note = build(:note, line_code: ' ')
expect { note.valid? }.to change(note, :line_code).to(nil)
end
end
end
......@@ -582,7 +582,58 @@ describe Project, models: true do
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey }
end
end
describe '.search' do
let(:project) { create(:project, description: 'kitten mittens') }
it 'returns projects with a matching name' do
expect(described_class.search(project.name)).to eq([project])
end
it 'returns projects with a partially matching name' do
expect(described_class.search(project.name[0..2])).to eq([project])
end
it 'returns projects with a matching name regardless of the casing' do
expect(described_class.search(project.name.upcase)).to eq([project])
end
it 'returns projects with a matching description' do
expect(described_class.search(project.description)).to eq([project])
end
it 'returns projects with a partially matching description' do
expect(described_class.search('kitten')).to eq([project])
end
it 'returns projects with a matching description regardless of the casing' do
expect(described_class.search('KITTEN')).to eq([project])
end
it 'returns projects with a matching path' do
expect(described_class.search(project.path)).to eq([project])
end
it 'returns projects with a partially matching path' do
expect(described_class.search(project.path[0..2])).to eq([project])
end
it 'returns projects with a matching path regardless of the casing' do
expect(described_class.search(project.path.upcase)).to eq([project])
end
it 'returns projects with a matching namespace name' do
expect(described_class.search(project.namespace.name)).to eq([project])
end
it 'returns projects with a partially matching namespace name' do
expect(described_class.search(project.namespace.name[0..2])).to eq([project])
end
it 'returns projects with a matching namespace name regardless of the casing' do
expect(described_class.search(project.namespace.name.upcase)).to eq([project])
end
end
describe '#rename_repo' do
......@@ -647,4 +698,20 @@ describe Project, models: true do
project.expire_caches_before_rename('foo')
end
end
describe '.search_by_title' do
let(:project) { create(:project, name: 'kittens') }
it 'returns projects with a matching name' do
expect(described_class.search_by_title(project.name)).to eq([project])
end
it 'returns projects with a partially matching name' do
expect(described_class.search_by_title('kitten')).to eq([project])
end
it 'returns projects with a matching name regardless of the casing' do
expect(described_class.search_by_title('KITTENS')).to eq([project])
end
end
end
......@@ -59,4 +59,48 @@ describe Snippet, models: true do
expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}"
end
end
describe '.search' do
let(:snippet) { create(:snippet) }
it 'returns snippets with a matching title' do
expect(described_class.search(snippet.title)).to eq([snippet])
end
it 'returns snippets with a partially matching title' do
expect(described_class.search(snippet.title[0..2])).to eq([snippet])
end
it 'returns snippets with a matching title regardless of the casing' do
expect(described_class.search(snippet.title.upcase)).to eq([snippet])
end
it 'returns snippets with a matching file name' do
expect(described_class.search(snippet.file_name)).to eq([snippet])
end
it 'returns snippets with a partially matching file name' do
expect(described_class.search(snippet.file_name[0..2])).to eq([snippet])
end
it 'returns snippets with a matching file name regardless of the casing' do
expect(described_class.search(snippet.file_name.upcase)).to eq([snippet])
end
end
describe '#search_code' do
let(:snippet) { create(:snippet, content: 'class Foo; end') }
it 'returns snippets with matching content' do
expect(described_class.search_code(snippet.content)).to eq([snippet])
end
it 'returns snippets with partially matching content' do
expect(described_class.search_code('class')).to eq([snippet])
end
it 'returns snippets with matching content regardless of the casing' do
expect(described_class.search_code('FOO')).to eq([snippet])
end
end
end
......@@ -463,17 +463,43 @@ describe User, models: true do
end
end
describe 'search' do
let(:user1) { create(:user, username: 'James', email: 'james@testing.com') }
let(:user2) { create(:user, username: 'jameson', email: 'jameson@example.com') }
it "should be case insensitive" do
expect(User.search(user1.username.upcase).to_a).to eq([user1])
expect(User.search(user1.username.downcase).to_a).to eq([user1])
expect(User.search(user2.username.upcase).to_a).to eq([user2])
expect(User.search(user2.username.downcase).to_a).to eq([user2])
expect(User.search(user1.username.downcase).to_a.size).to eq(2)
expect(User.search(user2.username.downcase).to_a.size).to eq(1)
describe '.search' do
let(:user) { create(:user) }
it 'returns users with a matching name' do
expect(described_class.search(user.name)).to eq([user])
end
it 'returns users with a partially matching name' do
expect(described_class.search(user.name[0..2])).to eq([user])
end
it 'returns users with a matching name regardless of the casing' do
expect(described_class.search(user.name.upcase)).to eq([user])
end
it 'returns users with a matching Email' do
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])
end
it 'returns users with a matching Email regardless of the casing' do
expect(described_class.search(user.email.upcase)).to eq([user])
end
it 'returns users with a matching username' do
expect(described_class.search(user.username)).to eq([user])
end
it 'returns users with a partially matching username' do
expect(described_class.search(user.username[0..2])).to eq([user])
end
it 'returns users with a matching username regardless of the casing' do
expect(described_class.search(user.username.upcase)).to eq([user])
end
end
......
......@@ -14,7 +14,6 @@ require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'shoulda/matchers'
require 'sidekiq/testing/inline'
require 'benchmark/ips'
require 'rspec/retry'
# Requires supporting ruby files with custom matchers and macros, etc,
......@@ -38,7 +37,6 @@ RSpec.configure do |config|
config.include ActiveJob::TestHelper
config.include StubGitlabCalls
config.include StubGitlabData
config.include BenchmarkMatchers, benchmark: true
config.infer_spec_type_from_file_location!
config.raise_errors_for_deprecations!
......
module BenchmarkMatchers
extend RSpec::Matchers::DSL
def self.included(into)
into.extend(ClassMethods)
end
matcher :iterate_per_second do |min_iterations|
supports_block_expectations
match do |block|
@max_stddev ||= 30
@entry = benchmark(&block)
expect(@entry.ips).to be >= min_iterations
expect(@entry.stddev_percentage).to be <= @max_stddev
end
chain :with_maximum_stddev do |value|
@max_stddev = value
end
description do
"run at least #{min_iterations} iterations per second"
end
failure_message do
ips = @entry.ips.round(2)
stddev = @entry.stddev_percentage.round(2)
"expected at least #{min_iterations} iterations per second " \
"with a maximum stddev of #{@max_stddev}%, instead of " \
"#{ips} iterations per second with a stddev of #{stddev}%"
end
end
# Benchmarks the given block and returns a Benchmark::IPS::Report::Entry.
def benchmark(&block)
report = Benchmark.ips(quiet: true) do |bench|
bench.report do
instance_eval(&block)
end
end
report.entries[0]
end
module ClassMethods
# Wraps around rspec's subject method so you can write:
#
# benchmark_subject { SomeClass.some_method }
#
# instead of:
#
# subject { -> { SomeClass.some_method } }
def benchmark_subject(&block)
subject { block }
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment