Commit 44dd4782 authored by Tomasz Maczukin's avatar Tomasz Maczukin

Merge branch 'master' into ci/api-builds

* master: (108 commits)
  Fix project transfer e-mail sending incorrect paths in e-mail notification
  Update CHANGELOG
  Use Gitlab::CurrentSettings for InfluxDB
  Write to InfluxDB directly via UDP
  Strip newlines from obfuscated SQL
  Add hotfix that allows to access build artifacts created before 8.3
  note votes methids implementation
  When reCAPTCHA is disabled, allow registrations to go through without a code
  Downcased user or email search for avatar_icon.
  Handle missing settings table for metrics
  Fix broken link in permissions page [ci skip]
  reCAPTCHA is configurable through Admin Settings, no reload needed.
  Fixed syntax in gitlab.yml.example
  Move InfluxDB settings to ApplicationSetting
  Fix spelling mistake, thanks Connor.
  Restart settings are moved too.
  Hotfix for builds trace data integrity
  add issue weight to contributing
  Added host option for InfluxDB
  Fixed styling of MetricsWorker specs
  ...
parents a17bf380 333d3d9e
......@@ -12,6 +12,7 @@ before_script:
spec:feature:
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
tags:
- ruby
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.4.0 (unreleased)
- Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu)
- Implement new UI for group page
- Implement search inside emoji picker
- Add API support for looking up a user by username (Stan Hu)
- Add project permissions to all project API endpoints (Stan Hu)
- Only allow group/project members to mention `@all`
- Expose Git's version in the admin area
- Add "Frequently used" category to emoji picker
- Add CAS support (tduehr)
- Add link to merge request on build detail page
- Revert back upvote and downvote button to the issue and MR pages
- Enable "Add key" button when user fills in a proper key (Stan Hu)
v 8.3.3 (unreleased)
- Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu)
v 8.3.2
- Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu)
- Add support for Google reCAPTCHA in user registration
v 8.3.1 (unreleased)
v 8.3.1
- Fix Error 500 when global milestones have slashes (Stan Hu)
- Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu)
- Fix LDAP identity and user retrieval when special characters are used
- Move Sidekiq-cron configuration to gitlab.yml
- Enable forcing Two-Factor authentication sitewide, with optional grace period
v 8.3.0
- Add CAS support (tduehr)
- Bump rack-attack to 4.3.1 for security fix (Stan Hu)
- API support for starred projects for authorized user (Zeger-Jan van de Weg)
- Add link to merge request on build detail page.
- Add open_issues_count to project API (Stan Hu)
- Expand character set of usernames created by Omniauth (Corey Hinshaw)
- Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg)
......@@ -75,7 +89,6 @@ v 8.3.0
- Do not show build status unless builds are enabled and `.gitlab-ci.yml` is present
- Persist runners registration token in database
- Fix online editor should not remove newlines at the end of the file
- Expose Git's version in the admin area
v 8.2.3
- Fix application settings cache not expiring after changes (Stan Hu)
......
......@@ -155,6 +155,28 @@ sudo -u git -H bundle exec rake gitlab:env:info)
```
### Issue weight
Issue weight allows us to get an idea of the amount of work required to solve
one or multiple issues. This makes it possible to schedule work more accurately.
You are encouraged to set the weight of any issue. Following the guidelines
below will make it easy to manage this, without unnecessary overhead.
1. Set weight for any issue at the earliest possible convenience
1. If you don't agree with a set weight, discuss with other developers until
consensus is reached about the weight
1. Issue weights are an abstract measurement of complexity of the issue. Do not
relate issue weight directly to time. This is called [anchoring](https://en.wikipedia.org/wiki/Anchoring)
and something you want to avoid.
1. Something that has a weight of 1 (or no weight) is really small and simple.
Something that is 9 is rewriting a large fundamental part of GitLab,
which might lead to many hard problems to solve. Changing some text in GitLab
is probably 1, adding a new Git Hook maybe 4 or 5, big features 7-9.
1. If something is very large, it should probably be split up in multiple
issues or chunks. You can simply not set the weight of a parent issue and set
weights to children issues.
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
......
......@@ -35,6 +35,9 @@ gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd'
gem 'rack-oauth2', '~> 1.2.1'
# reCAPTCHA protection
gem 'recaptcha', require: 'recaptcha/rails'
# Two-factor authentication
gem 'devise-two-factor', '~> 2.0.0'
gem 'rqrcode-rails3', '~> 0.1.7'
......@@ -212,9 +215,17 @@ gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
# Metrics
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false
gem 'connection_pool', '~> 2.0', require: false
end
group :development do
gem "foreman"
gem 'brakeman', '3.0.1', require: false
gem 'brakeman', '~> 3.1.0', require: false
gem "annotate", "~> 2.6.0"
gem "letter_opener", '~> 1.1.2'
......
......@@ -49,6 +49,7 @@ GEM
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
allocations (1.0.1)
annotate (2.6.10)
activerecord (>= 3.2, <= 4.3)
rake (~> 10.4)
......@@ -65,7 +66,7 @@ GEM
attr_encrypted (1.3.4)
encryptor (>= 1.3.0)
attr_required (1.0.0)
autoprefixer-rails (6.1.1)
autoprefixer-rails (6.1.2)
execjs
json
awesome_print (1.2.0)
......@@ -84,15 +85,17 @@ GEM
bootstrap-sass (3.3.5)
autoprefixer-rails (>= 5.0.0.1)
sass (>= 3.2.19)
brakeman (3.0.1)
brakeman (3.1.4)
erubis (~> 2.6)
fastercsv (~> 1.5)
haml (>= 3.0, < 5.0)
highline (~> 1.6.20)
highline (>= 1.6.20, < 2.0)
multi_json (~> 1.2)
ruby2ruby (~> 2.1.1)
ruby_parser (~> 3.5.0)
ruby2ruby (>= 2.1.1, < 2.3.0)
ruby_parser (~> 3.7.0)
safe_yaml (>= 1.0)
sass (~> 3.0)
slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4)
browser (1.0.1)
builder (3.2.2)
......@@ -102,7 +105,7 @@ GEM
bundler-audit (0.4.0)
bundler (~> 1.2)
thor (~> 0.18)
byebug (8.2.0)
byebug (8.2.1)
cal-heatmap-rails (0.0.1)
capybara (2.4.4)
mime-types (>= 1.16)
......@@ -117,6 +120,7 @@ GEM
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
json (>= 1.7)
cause (0.1)
charlock_holmes (0.7.3)
chunky_png (1.3.5)
cliver (0.3.2)
......@@ -140,10 +144,10 @@ GEM
term-ansicolor (~> 1.3)
thor (~> 0.19.1)
tins (~> 1.6.0)
crack (0.4.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
creole (0.5.0)
d3_rails (3.5.6)
d3_rails (3.5.11)
railties (>= 3.1.0)
daemons (1.2.3)
database_cleaner (1.4.1)
......@@ -230,7 +234,7 @@ GEM
ipaddress (~> 0.5)
nokogiri (~> 1.5, >= 1.5.11)
opennebula
fog-brightbox (0.9.0)
fog-brightbox (0.10.1)
fog-core (~> 1.22)
fog-json
inflecto (~> 0.0.2)
......@@ -249,7 +253,7 @@ GEM
fog-core (>= 1.21.0)
fog-json
fog-xml (>= 0.0.1)
fog-sakuracloud (1.4.0)
fog-sakuracloud (1.5.0)
fog-core
fog-json
fog-softlayer (1.0.2)
......@@ -277,11 +281,11 @@ GEM
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
gemojione (2.1.0)
gemojione (2.1.1)
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
github-linguist (4.7.2)
github-linguist (4.7.3)
charlock_holmes (~> 0.7.3)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
......@@ -298,7 +302,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.2.0)
gemojione (~> 2.1)
gitlab_git (7.2.21)
gitlab_git (7.2.22)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -347,7 +351,7 @@ GEM
html2haml (>= 1.0.1)
railties (>= 4.0.1)
hashie (3.4.3)
highline (1.6.21)
highline (1.7.8)
hike (1.2.3)
hipchat (1.5.2)
httparty
......@@ -370,6 +374,9 @@ GEM
i18n (0.7.0)
ice_nine (0.11.1)
inflecto (0.0.2)
influxdb (0.2.3)
cause
json
ipaddress (0.8.0)
jquery-atwho-rails (1.3.2)
jquery-rails (4.0.5)
......@@ -417,7 +424,7 @@ GEM
net-ldap (0.12.1)
net-ssh (3.0.1)
netrc (0.11.0)
newrelic-grape (2.0.0)
newrelic-grape (2.1.0)
grape
newrelic_rpm
newrelic_rpm (3.9.4.245)
......@@ -566,6 +573,8 @@ GEM
trollop
rdoc (3.12.2)
json (~> 1.4)
recaptcha (1.0.2)
json
redcarpet (3.3.3)
redis (3.2.2)
redis-actionpack (4.0.1)
......@@ -636,10 +645,10 @@ GEM
ruby-saml (1.0.0)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
ruby2ruby (2.1.4)
ruby2ruby (2.2.0)
ruby_parser (~> 3.1)
sexp_processor (~> 4.0)
ruby_parser (3.5.0)
ruby_parser (3.7.2)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
......@@ -693,6 +702,9 @@ GEM
tilt (>= 1.3, < 3)
six (0.2.0)
slack-notifier (1.2.1)
slim (3.0.6)
temple (~> 0.7.3)
tilt (>= 1.3.3, < 2.1)
slop (3.6.0)
spinach (0.8.10)
colorize
......@@ -734,6 +746,7 @@ GEM
railties (>= 3.2.5, < 5)
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
temple (0.7.6)
term-ansicolor (1.3.2)
tins (~> 1.0)
terminal-table (1.5.2)
......@@ -789,7 +802,7 @@ GEM
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
warden (1.2.3)
warden (1.2.4)
rack (>= 1.0)
web-console (2.2.1)
activemodel (>= 4.0)
......@@ -820,6 +833,7 @@ DEPENDENCIES
acts-as-taggable-on (~> 3.4)
addressable (~> 2.3.8)
after_commit_queue
allocations (~> 1.0)
annotate (~> 2.6.0)
asana (~> 0.4.0)
asciidoctor (~> 1.5.2)
......@@ -830,7 +844,7 @@ DEPENDENCIES
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.0)
brakeman (= 3.0.1)
brakeman (~> 3.1.0)
browser (~> 1.0.0)
bullet
bundler-audit
......@@ -842,6 +856,7 @@ DEPENDENCIES
charlock_holmes (~> 0.7.3)
coffee-rails (~> 4.1.0)
colorize (~> 0.7.0)
connection_pool (~> 2.0)
coveralls (~> 0.8.2)
creole (~> 0.5.0)
d3_rails (~> 3.5.5)
......@@ -879,6 +894,7 @@ DEPENDENCIES
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
influxdb (~> 0.2)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.0.0)
jquery-scrollto-rails (~> 1.4.3)
......@@ -887,6 +903,7 @@ DEPENDENCIES
kaminari (~> 0.16.3)
letter_opener (~> 1.1.2)
mail_room (~> 0.6.1)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16)
......@@ -924,6 +941,7 @@ DEPENDENCIES
raphael-rails (~> 2.1.2)
rblineprof
rdoc (~> 3.6)
recaptcha
redcarpet (~> 3.3.3)
redis-namespace
redis-rails (~> 4.0.0)
......
......@@ -43,15 +43,19 @@ class @AwardsHandler
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter")
emojiIcon = counter.parent()
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
counter.parent().removeClass("active")
emojiIcon.removeClass("active")
@removeMeFromAuthorList(emoji)
else if emoji =="thumbsup" || emoji == "thumbsdown"
emojiIcon.tooltip("destroy")
counter.text(0)
emojiIcon.removeClass("active")
else
award = counter.parent()
award.tooltip("destroy")
award.remove()
emojiIcon.tooltip("destroy")
emojiIcon.remove()
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
......@@ -127,12 +131,10 @@ class @AwardsHandler
getFrequentlyUsedEmojis: ->
frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",")
frequently_used_emojis = ["thumbsup", "thumbsdown"].concat(frequently_used_emojis)
_.compact(_.uniq(frequently_used_emojis))
renderFrequentlyUsedBlock: ->
if $.cookie('frequently_used_emojis')
frequently_used_emojis = @getFrequentlyUsedEmojis()
ul = $("<ul>")
......
......@@ -49,7 +49,7 @@ class Dispatcher
new DropzoneInput($('.release-form'))
when 'projects:merge_requests:show'
new Diff()
shortcut_handler = new ShortcutsIssuable()
shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
when "projects:merge_requests:diffs"
new Diff()
......
#= require flash
#= require jquery.waitforimages
#= require task_list
......@@ -6,13 +7,44 @@ class @Issue
# Prevent duplicate event bindings
@disableTaskList()
if $("a.btn-close").length
if $('a.btn-close').length
@initTaskList()
@initIssueBtnEventListeners()
initTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
initIssueBtnEventListeners: ->
issueFailMessage = 'Unable to update this issue at this time.'
$('a.btn-close, a.btn-reopen').on 'click', (e) ->
e.preventDefault()
e.stopImmediatePropagation()
$this = $(this)
isClose = $this.hasClass('btn-close')
$this.prop('disabled', true)
url = $this.attr('href')
$.ajax
type: 'PUT'
url: url,
error: (jqXHR, textStatus, errorThrown) ->
issueStatus = if isClose then 'close' else 'open'
new Flash(issueFailMessage, 'alert')
success: (data, textStatus, jqXHR) ->
if data.saved
$this.addClass('hidden')
if isClose
$('a.btn-reopen').removeClass('hidden')
$('div.status-box-closed').removeClass('hidden')
$('div.status-box-open').addClass('hidden')
else
$('a.btn-close').removeClass('hidden')
$('div.status-box-closed').addClass('hidden')
$('div.status-box-open').removeClass('hidden')
else
new Flash(issueFailMessage, 'alert')
$this.prop('disabled', false)
disableTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
......
......@@ -17,8 +17,7 @@ class @Shortcuts
dataType: 'script',
success: (e) ->
if location and location.length > 0
for l in location
$(l).show()
$(l).show() for l in location
else
$('.hidden-shortcut').show()
$('.js-more-help-button').remove()
......@@ -28,3 +27,8 @@ class @Shortcuts
@focusSearch: (e) ->
$('#search').focus()
e.preventDefault()
$(document).on 'click.more_help', '.js-more-help-button', (e) ->
$(@).remove()
$('.hidden-shortcut').show()
e.preventDefault()
......@@ -117,5 +117,5 @@ class @UsersSelect
callback(users)
buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root?
url = gon.relative_url_root.replace(/\/$/, '') + url if gon.relative_url_root?
return url
......@@ -49,6 +49,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:default_branch_protection,
:signup_enabled,
:signin_enabled,
:require_two_factor_authentication,
:two_factor_grace_period,
:gravatar_enabled,
:twitter_sharing_enabled,
:sign_in_text,
......@@ -65,6 +67,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:user_oauth_applications,
:shared_runners_enabled,
:max_artifacts_size,
:metrics_enabled,
:metrics_host,
:metrics_port,
:metrics_username,
:metrics_password,
:metrics_pool_size,
:metrics_timeout,
:metrics_method_call_threshold,
:recaptcha_enabled,
:recaptcha_site_key,
:recaptcha_private_key,
restricted_visibility_levels: [],
import_sources: []
)
......
......@@ -13,6 +13,7 @@ class ApplicationController < ActionController::Base
before_action :validate_user_service_ticket!
before_action :reject_blocked!
before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check
before_action :default_headers
before_action :add_gon_variables
......@@ -223,6 +224,12 @@ class ApplicationController < ActionController::Base
end
end
def check_2fa_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor?
redirect_to new_profile_two_factor_auth_path
end
end
def ldap_security_check
if current_user && current_user.requires_ldap_check?
unless Gitlab::LDAP::Access.allowed?(current_user)
......@@ -357,6 +364,23 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('git')
end
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication
end
def two_factor_grace_period
current_application_settings.two_factor_grace_period
end
def two_factor_grace_period_expired?
date = current_user.otp_grace_period_started_at
date && (date + two_factor_grace_period.hours) < Time.current
end
def skip_two_factor?
session[:skip_tfa] && session[:skip_tfa] > Time.current
end
def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections
......
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement
def new
unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32)
current_user.save!
end
unless current_user.otp_grace_period_started_at && two_factor_grace_period
current_user.otp_grace_period_started_at = Time.current
end
current_user.save! if current_user.changed?
if two_factor_grace_period_expired?
flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.'
else
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}."
end
@qr_code = build_qr_code
......@@ -34,6 +48,15 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
redirect_to profile_account_path
end
def skip
if two_factor_grace_period_expired?
redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup'
else
session[:skip_tfa] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
redirect_to root_path
end
end
private
def build_qr_code
......
......@@ -9,7 +9,7 @@ class Projects::CommitsController < Projects::ApplicationController
def show
@repo = @project.repository
@limit, @offset = (params[:limit] || 40), (params[:offset] || 0)
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
@commits = @repo.commits(@ref, @path, @limit, @offset)
@note_counts = project.notes.where(commit_id: @commits.map(&:id)).
......
......@@ -178,7 +178,7 @@ class ProjectsController < ApplicationController
def markdown_preview
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
ext.analyze(text)
render json: {
......
class RegistrationsController < Devise::RegistrationsController
before_action :signup_enabled?
include Recaptcha::Verify
def new
redirect_to(new_user_session_path)
end
def create
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
super
else
flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code."
flash.delete :recaptcha_error
render action: 'new'
end
end
def destroy
DeleteUserService.new(current_user).execute(current_user)
......@@ -38,4 +49,16 @@ class RegistrationsController < Devise::RegistrationsController
def sign_up_params
params.require(:user).permit(:username, :email, :name, :password, :password_confirmation)
end
def resource_name
:user
end
def resource
@resource ||= User.new(sign_up_params)
end
def devise_mapping
@devise_mapping ||= Devise.mappings[:user]
end
end
class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor
include Recaptcha::ClientHelper
prepend_before_action :authenticate_with_two_factor, only: [:create]
prepend_before_action :store_redirect_path, only: [:new]
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
def new
if Gitlab.config.ldap.enabled
......@@ -107,4 +109,8 @@ class SessionsController < Devise::SessionsController
AuditEventService.new(user, user, options).
for_authentication.security_event
end
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
end
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)
user = User.find_by(email: user_or_email.downcase)
end
if user
......
......@@ -50,5 +50,17 @@ module AuthHelper
current_user.identities.exists?(provider: provider.to_s)
end
def two_factor_skippable?
current_application_settings.require_two_factor_authentication &&
!current_user.two_factor_enabled &&
current_application_settings.two_factor_grace_period &&
!two_factor_grace_period_expired?
end
def two_factor_grace_period_expired?
current_user.otp_grace_period_started_at &&
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
end
extend self
end
......@@ -69,6 +69,10 @@ module IssuesHelper
end
end
def issue_button_visibility(issue, closed)
return 'hidden' if issue.closed? == closed
end
def issue_to_atom(xml, issue)
xml.entry do
xml.id namespace_project_issue_url(issue.project.namespace,
......@@ -120,6 +124,18 @@ module IssuesHelper
end
end
def awards_sort(awards)
awards.sort_by do |award, notes|
if award == "thumbsup"
0
elsif award == "thumbsdown"
1
else
2
end
end.to_h
end
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
end
......@@ -8,6 +8,80 @@ module PageLayoutHelper
@page_title.join(" \u00b7 ")
end
# Define or get a description for the current page
#
# description - String (default: nil)
#
# If this helper is called multiple times with an argument, only the last
# description will be returned when called without an argument. Descriptions
# have newlines replaced with spaces and all HTML tags are sanitized.
#
# Examples:
#
# page_description # => "GitLab Community Edition"
# page_description("Foo")
# page_description # => "Foo"
#
# page_description("<b>Bar</b>\nBaz")
# page_description # => "Bar Baz"
#
# Returns an HTML-safe String.
def page_description(description = nil)
@page_description ||= page_description_default
if description.present?
@page_description = description.squish
else
sanitize(@page_description, tags: []).truncate_words(30)
end
end
# Default value for page_description when one hasn't been defined manually by
# a view
def page_description_default
if @project
@project.description || brand_title
else
brand_title
end
end
def page_image
default = image_url('gitlab_logo.png')
if @project
@project.avatar_url || default
elsif @user
avatar_icon(@user)
else
default
end
end
# Define or get attributes to be used as Twitter card metadata
#
# map - Hash of label => data pairs. Keys become labels, values become data
#
# Raises ArgumentError if given more than two attributes
def page_card_attributes(map = {})
raise ArgumentError, 'cannot provide more than two attributes' if map.length > 2
@page_card_attributes ||= {}
@page_card_attributes = map.reject { |_,v| v.blank? } if map.present?
@page_card_attributes
end
def page_card_meta_tags
tags = ''
page_card_attributes.each_with_index do |pair, i|
tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0])
tags << tag(:meta, property: "twitter:data#{i + 1}", content: pair[1])
end
tags.html_safe
end
def header_title(title = nil, title_url = nil)
if title
@header_title = title
......
......@@ -132,14 +132,14 @@ class Ability
end
def public_project_rules
project_guest_rules + [
@public_project_rules ||= project_guest_rules + [
:download_code,
:fork_project
]
end
def project_guest_rules
[
@project_guest_rules ||= [
:read_project,
:read_wiki,
:read_issue,
......@@ -157,7 +157,7 @@ class Ability
end
def project_report_rules
project_guest_rules + [
@project_report_rules ||= project_guest_rules + [
:create_commit_status,
:read_commit_statuses,
:download_code,
......@@ -170,7 +170,7 @@ class Ability
end
def project_dev_rules
project_report_rules + [
@project_dev_rules ||= project_report_rules + [
:admin_merge_request,
:create_merge_request,
:create_wiki,
......@@ -181,7 +181,7 @@ class Ability
end
def project_archived_rules
[
@project_archived_rules ||= [
:create_merge_request,
:push_code,
:push_code_to_protected_branches,
......@@ -191,7 +191,7 @@ class Ability
end
def project_master_rules
project_dev_rules + [
@project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches,
:update_project_snippet,
:update_merge_request,
......@@ -206,7 +206,7 @@ class Ability
end
def project_admin_rules
project_master_rules + [
@project_admin_rules ||= project_master_rules + [
:change_namespace,
:change_visibility_level,
:rename_project,
......
......@@ -28,6 +28,8 @@
# shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null
# runners_registration_token :string(255)
# require_two_factor_authentication :boolean default(TRUE)
# two_factor_grace_period :integer default(48)
#
class ApplicationSetting < ActiveRecord::Base
......@@ -58,6 +60,17 @@ class ApplicationSetting < ActiveRecord::Base
allow_blank: true,
email: true
validates :two_factor_grace_period,
numericality: { greater_than_or_equal_to: 0 }
validates :recaptcha_site_key,
presence: true,
if: :recaptcha_enabled
validates :recaptcha_private_key,
presence: true,
if: :recaptcha_enabled
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
......@@ -112,6 +125,8 @@ class ApplicationSetting < ActiveRecord::Base
import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48
)
end
......
......@@ -194,8 +194,11 @@ module Ci
end
def raw_trace
if File.exist?(path_to_trace)
if File.file?(path_to_trace)
File.read(path_to_trace)
elsif project.ci_id && File.file?(old_path_to_trace)
# Temporary fix for build trace data integrity
File.read(old_path_to_trace)
else
# backward compatibility
read_attribute :trace
......@@ -212,8 +215,8 @@ module Ci
end
def trace=(trace)
unless Dir.exists? dir_to_trace
FileUtils.mkdir_p dir_to_trace
unless Dir.exists?(dir_to_trace)
FileUtils.mkdir_p(dir_to_trace)
end
File.write(path_to_trace, trace)
......@@ -231,6 +234,55 @@ module Ci
"#{dir_to_trace}/#{id}.log"
end
##
# Deprecated
#
# This is a hotfix for CI build data integrity, see #4246
# Should be removed in 8.4, after CI files migration has been done.
#
def old_dir_to_trace
File.join(
Settings.gitlab_ci.builds_path,
created_at.utc.strftime("%Y_%m"),
project.ci_id.to_s
)
end
##
# Deprecated
#
# This is a hotfix for CI build data integrity, see #4246
# Should be removed in 8.4, after CI files migration has been done.
#
def old_path_to_trace
"#{old_dir_to_trace}/#{id}.log"
end
##
# Deprecated
#
# This contains a hotfix for CI build data integrity, see #4246
#
# This method is used by `ArtifactUploader` to create a store_dir.
# Warning: Uploader uses it after AND before file has been stored.
#
# This method returns old path to artifacts only if it already exists.
#
def artifacts_path
old = File.join(created_at.utc.strftime('%Y_%m'),
project.ci_id.to_s,
id.to_s)
old_store = File.join(ArtifactUploader.artifacts_path, old)
return old if project.ci_id && File.directory?(old_store)
File.join(
created_at.utc.strftime('%Y_%m'),
project.id.to_s,
id.to_s
)
end
def token
project.runners_token
end
......
......@@ -95,14 +95,12 @@ module Issuable
opened? || reopened?
end
# Deprecated. Still exists to preserve API compatibility.
def downvotes
0
notes.awards.where(note: "thumbsdown").count
end
# Deprecated. Still exists to preserve API compatibility.
def upvotes
0
notes.awards.where(note: "thumbsup").count
end
def subscribed?(user)
......@@ -161,6 +159,14 @@ module Issuable
self.class.to_s.underscore
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee.try(:name)
}
end
def notes_with_associations
notes.includes(:author, :project)
end
......
......@@ -44,7 +44,7 @@ module Mentionable
end
def all_references(current_user = self.author, text = nil)
ext = Gitlab::ReferenceExtractor.new(self.project, current_user)
ext = Gitlab::ReferenceExtractor.new(self.project, current_user, self.author)
if text
ext.analyze(text)
......
......@@ -107,9 +107,16 @@ class Note < ActiveRecord::Base
end
def grouped_awards
notes = {}
awards.select(:note).distinct.map do |note|
[ note.note, where(note: note.note) ]
notes[note.note] = where(note: note.note)
end
notes["thumbsup"] ||= Note.none
notes["thumbsdown"] ||= Note.none
notes
end
end
......@@ -339,14 +346,12 @@ class Note < ActiveRecord::Base
read_attribute(:system)
end
# Deprecated. Still exists to preserve API compatibility.
def downvote?
false
is_award && note == "thumbsdown"
end
# Deprecated. Still exists to preserve API compatibility.
def upvote?
false
is_award && note == "thumbsup"
end
def editable?
......
......@@ -555,7 +555,9 @@ class Project < ActiveRecord::Base
end
def send_move_instructions(old_path_with_namespace)
NotificationService.new.project_was_moved(self, old_path_with_namespace)
# New project path needs to be committed to the DB or notification will
# retrieve stale information
run_after_commit { NotificationService.new.project_was_moved(self, old_path_with_namespace) }
end
def owner
......
......@@ -76,7 +76,9 @@ class Repository
path: path,
limit: limit,
offset: offset,
follow: path.present?
# --follow doesn't play well with --skip. See:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
follow: false
}
commits = Gitlab::Git::Commit.where(options)
......
......@@ -20,16 +20,12 @@ class ArtifactUploader < CarrierWave::Uploader::Base
@build, @field = build, field
end
def artifacts_path
File.join(build.created_at.utc.strftime('%Y_%m'), build.project.id.to_s, build.id.to_s)
end
def store_dir
File.join(ArtifactUploader.artifacts_path, artifacts_path)
File.join(self.class.artifacts_path, @build.artifacts_path)
end
def cache_dir
File.join(ArtifactUploader.artifacts_cache_path, artifacts_path)
File.join(self.class.artifacts_cache_path, @build.artifacts_path)
end
def file_storage?
......
......@@ -104,6 +104,18 @@
= f.label :signin_enabled do
= f.check_box :signin_enabled
Sign-in enabled
.form-group
= f.label :two_factor_authentication, 'Two-Factor authentication', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :require_two_factor_authentication do
= f.check_box :require_two_factor_authentication
Require all users to setup Two-Factor authentication
.form-group
= f.label :two_factor_authentication, 'Two-Factor grace period (hours)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
.form-group
= f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
.col-sm-10
......@@ -144,5 +156,82 @@
.col-sm-10
= f.number_field :max_artifacts_size, class: 'form-control'
%fieldset
%legend Metrics
%p
These settings require a restart to take effect.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :metrics_enabled do
= f.check_box :metrics_enabled
Enable InfluxDB Metrics
.form-group
= f.label :metrics_host, 'InfluxDB host', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
.form-group
= f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
.help-block
The UDP port to use for connecting to InfluxDB. InfluxDB requires that
your server configuration specifies a database to store data in when
sending messages to this port, without it metrics data will not be
saved.
.form-group
= f.label :metrics_username, 'InfluxDB username', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :metrics_username, class: 'form-control'
.form-group
= f.label :metrics_password, 'InfluxDB password', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :metrics_password, class: 'form-control'
.form-group
= f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :metrics_pool_size, class: 'form-control'
.help-block
The amount of InfluxDB connections to open. Connections are opened
lazily. Users using multi-threaded application servers should ensure
enough connections are available (at minimum the amount of application
server threads).
.form-group
= f.label :metrics_timeout, 'Connection timeout', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :metrics_timeout, class: 'form-control'
.help-block
The amount of seconds after which an InfluxDB connection will time
out.
.form-group
= f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :metrics_method_call_threshold, class: 'form-control'
.help-block
A method call is only tracked when it takes longer to complete than
the given amount of milliseconds.
%fieldset
%legend Spam and Anti-bot Protection
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :recaptcha_enabled do
= f.check_box :recaptcha_enabled
Enable reCAPTCHA
%span.help-block#recaptcha_help_block Helps preventing bots from creating accounts
.form-group
= f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_site_key, class: 'form-control'
.help-block
Generate site and private keys here:
%a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha
.form-group
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_private_key, class: 'form-control'
.form-actions
= f.submit 'Save', class: 'btn btn-primary'
......@@ -6,17 +6,21 @@
.login-heading
%h3 Create an account
.login-body
- user = params[:user].present? ? params[:user] : {}
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
.devise-errors
= devise_error_messages!
%div
= f.text_field :name, class: "form-control top", placeholder: "Name", required: true
= f.text_field :name, class: "form-control top", value: user[:name], placeholder: "Name", required: true
%div
= f.text_field :username, class: "form-control middle", placeholder: "Username", required: true
= f.text_field :username, class: "form-control middle", value: user[:username], placeholder: "Username", required: true
%div
= f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
= f.email_field :email, class: "form-control middle", value: user[:email], placeholder: "Email", required: true
.form-group.append-bottom-20#password-strength
= f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true
= f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true
%div
- if current_application_settings.recaptcha_enabled
= recaptcha_tags
%div
= f.submit "Sign up", class: "btn-create btn"
......
......@@ -31,7 +31,7 @@
.col-sm-10
.checkbox
= f.check_box :public
%span.descr Make this group public (even if there is no any public project inside this group)
%span.descr Make this group public (even if there are no public projects inside this group)
.form-actions
= f.submit 'Save group', class: "btn btn-save"
......
......@@ -219,11 +219,3 @@
%td.shortcut
.key r
%td Reply (quoting selected text)
:javascript
$('.js-more-help-button').click(function (e) {
$(this).remove()l
$('.hidden-shortcut').show();
e.preventDefault();
});
- page_title "GitLab"
%head
%head{prefix: "og: http://ogp.me/ns#"}
%meta{charset: "utf-8"}
%meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
%meta{content: "GitLab Community Edition", name: "description"}
%meta{name: 'referrer', content: 'origin-when-cross-origin'}
%meta{name: "description", content: page_description}
-# Open Graph - http://ogp.me/
%meta{property: 'og:type', content: "object"}
%meta{property: 'og:site_name', content: "GitLab"}
%meta{property: 'og:title', content: page_title}
%meta{property: 'og:description', content: page_description}
%meta{property: 'og:image', content: page_image}
%meta{property: 'og:url', content: request.base_url + request.fullpath}
-# Twitter Card - https://dev.twitter.com/cards/types/summary
%meta{property: 'twitter:card', content: "summary"}
%meta{property: 'twitter:title', content: page_title}
%meta{property: 'twitter:description', content: page_description}
%meta{property: 'twitter:image', content: page_image}
= page_card_meta_tags
- page_title "GitLab"
%title= page_title
= favicon_link_tag 'favicon.ico'
......
......@@ -12,6 +12,6 @@
comment = val.match(/^\S+ \S+ (.+)\n?$/);
if( comment && comment.length > 1 && title.val() == '' ){
$('#key_title').val( comment[1] );
$('#key_title').val( comment[1] ).change();
}
});
......@@ -38,3 +38,4 @@
= text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true
.form-actions
= submit_tag 'Submit', class: 'btn btn-success'
= link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
= render "header_title"
.issue
.detail-page-header
.status-box{ class: status_box_class(@issue) }
- if @issue.closed?
Closed
- else
Open
.status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} Closed
.status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} Open
%span.identifier
Issue ##{@issue.iid}
%span.creator
......@@ -27,10 +27,8 @@
= icon('plus')
New Issue
- if can?(current_user, :update_issue, @issue)
- if @issue.closed?
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen'
- else
= link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close Issue'
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
= link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
= icon('pencil-square-o')
......
- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
= render "header_title"
- if params[:view] == 'parallel'
......
- page_title @milestone.title, "Milestones"
- page_description @milestone.description
= render "header_title"
.detail-page-header
......
- page_title @user.name
- page_description @user.bio
- header_title @user.name, user_path(@user)
= content_for :meta_tags do
......
.awards.votes-block
- votable.notes.awards.grouped_awards.each do |emoji, notes|
- 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)}
= emoji_icon(emoji)
.counter
......
<%= ENV['RAILS_ENV'] %>:
adapter: <%= ENV['GITLAB_DATABASE_ADAPTER'] || 'postgresql' %>
encoding: <%= ENV['GITLAB_DATABASE_ENCODING'] || 'unicode' %>
database: <%= ENV['GITLAB_DATABASE_DATABASE'] || "gitlab_#{ENV['RAILS_ENV']}" %>
pool: <%= ENV['GITLAB_DATABASE_POOL'] || '10' %>
username: <%= ENV['GITLAB_DATABASE_USERNAME'] || 'root' %>
password: <%= ENV['GITLAB_DATABASE_PASSWORD'] || '' %>
host: <%= ENV['GITLAB_DATABASE_HOST'] || 'localhost' %>
port: <%= ENV['GITLAB_DATABASE_PORT'] || '5432' %>
......@@ -4,8 +4,8 @@
#
########################### NOTE #####################################
# This file should not receive new settings. All configuration options #
# that do not require an application restart are being moved to #
# ApplicationSetting model! #
# * are being moved to ApplicationSetting model! #
# If a setting requires an application restart say so in that screen. #
# If you change this file in a Merge Request, please also create #
# a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests #
########################################################################
......
......@@ -131,6 +131,7 @@ Settings.omniauth.cas3['session_duration'] ||= 8.hours
Settings.omniauth['session_tickets'] ||= Settingslogic.new({})
Settings.omniauth.session_tickets['cas3'] = 'ticket'
Settings['shared'] ||= Settingslogic.new({})
Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root)
......@@ -144,7 +145,7 @@ Settings.gitlab['default_projects_limit'] ||= 10
Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
Settings.gitlab['host'] ||= 'localhost'
Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80
......
if Gitlab::Metrics.enabled?
require 'influxdb'
require 'socket'
require 'connection_pool'
require 'method_source'
# These are manually require'd so the classes are registered properly with
# ActiveSupport.
require 'gitlab/metrics/subscribers/action_view'
require 'gitlab/metrics/subscribers/active_record'
Gitlab::Application.configure do |config|
config.middleware.use(Gitlab::Metrics::RackMiddleware)
end
Sidekiq.configure_server do |config|
config.server_middleware do |chain|
chain.add Gitlab::Metrics::SidekiqMiddleware
end
end
# This instruments all methods residing in app/models that (appear to) use any
# of the ActiveRecord methods. This has to take place _after_ initializing as
# for some unknown reason calling eager_load! earlier breaks Devise.
Gitlab::Application.config.after_initialize do
Rails.application.eager_load!
models = Rails.root.join('app', 'models').to_s
regex = Regexp.union(
ActiveRecord::Querying.public_instance_methods(false).map(&:to_s)
)
Gitlab::Metrics::Instrumentation.
instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
# Instrumenting the ApplicationSetting class can lead to an infinite
# loop. Since the data is cached any way we don't really need to
# instrument it.
if klass == ApplicationSetting
false
else
loc = method.source_location
loc && loc[0].start_with?(models) && method.source =~ regex
end
end
end
Gitlab::Metrics::Instrumentation.configure do |config|
config.instrument_instance_methods(Gitlab::Shell)
config.instrument_methods(Gitlab::Git)
Gitlab::Git.constants.each do |name|
const = Gitlab::Git.const_get(name)
config.instrument_methods(const) if const.is_a?(Module)
end
end
GC::Profiler.enable
Gitlab::Metrics::Sampler.new.start
end
......@@ -297,6 +297,7 @@ Rails.application.routes.draw do
resource :two_factor_auth, only: [:new, :create, :destroy] do
member do
post :codes
patch :skip
end
end
end
......
class AddTfaToApplicationSettings < ActiveRecord::Migration
def change
change_table :application_settings do |t|
t.boolean :require_two_factor_authentication, default: false
t.integer :two_factor_grace_period, default: 48
end
end
end
class AddTfaAdditionalFields < ActiveRecord::Migration
def change
change_table :users do |t|
t.datetime :otp_grace_period_started_at, null: true
end
end
end
class InfluxdbSettings < ActiveRecord::Migration
def change
add_column :application_settings, :metrics_enabled, :boolean, default: false
add_column :application_settings, :metrics_host, :string,
default: 'localhost'
add_column :application_settings, :metrics_database, :string,
default: 'gitlab'
add_column :application_settings, :metrics_username, :string
add_column :application_settings, :metrics_password, :string
add_column :application_settings, :metrics_pool_size, :integer, default: 16
add_column :application_settings, :metrics_timeout, :integer, default: 10
add_column :application_settings, :metrics_method_call_threshold,
:integer, default: 10
end
end
class AddRecaptchaToApplicationSettings < ActiveRecord::Migration
def change
change_table :application_settings do |t|
t.boolean :recaptcha_enabled, default: false
t.string :recaptcha_site_key
t.string :recaptcha_private_key
end
end
end
class InfluxdbUdpPortSetting < ActiveRecord::Migration
def change
add_column :application_settings, :metrics_port, :integer, default: 8089
end
end
class InfluxdbRemoteDatabaseSetting < ActiveRecord::Migration
def change
remove_column :application_settings, :metrics_database
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20151224123230) do
ActiveRecord::Schema.define(version: 20151229112614) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -50,6 +50,19 @@ ActiveRecord::Schema.define(version: 20151224123230) do
t.boolean "shared_runners_enabled", default: true, null: false
t.integer "max_artifacts_size", default: 100, null: false
t.string "runners_registration_token"
t.boolean "require_two_factor_authentication", default: false
t.integer "two_factor_grace_period", default: 48
t.boolean "metrics_enabled", default: false
t.string "metrics_host", default: "localhost"
t.string "metrics_username"
t.string "metrics_password"
t.integer "metrics_pool_size", default: 16
t.integer "metrics_timeout", default: 10
t.integer "metrics_method_call_threshold", default: 10
t.boolean "recaptcha_enabled", default: false
t.string "recaptcha_site_key"
t.string "recaptcha_private_key"
t.integer "metrics_port", default: 8089
end
create_table "audit_events", force: :cascade do |t|
......@@ -838,6 +851,7 @@ ActiveRecord::Schema.define(version: 20151224123230) do
t.integer "layout", default: 0
t.boolean "hide_project_limit", default: false
t.string "unlock_token"
t.datetime "otp_grace_period_started_at"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
......
......@@ -28,17 +28,18 @@
- [Using SSH keys](ci/ssh_keys/README.md)
- [User permissions](ci/permissions/README.md)
- [API](ci/api/README.md)
- [Triggering builds through the API](ci/triggers/README.md)
### CI Languages
+ [Testing PHP](ci/languages/php.md)
- [Testing PHP](ci/languages/php.md)
### CI Services
+ [Using MySQL](ci/services/mysql.md)
+ [Using PostgreSQL](ci/services/postgres.md)
+ [Using Redis](ci/services/redis.md)
+ [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
- [Using MySQL](ci/services/mysql.md)
- [Using PostgreSQL](ci/services/postgres.md)
- [Using Redis](ci/services/redis.md)
- [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
### CI Examples
......@@ -55,6 +56,7 @@
- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
- [Log system](logs/logs.md) Log system.
- [Environmental Variables](administration/environmental_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects.
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
......
# Environment Variables
## Introduction
Commonly people configure GitLab via the gitlab.rb configuration file in the Omnibus package.
But if you prefer to use environment variables we allow that too.
## Supported environment variables
Variable | Type | Explanation
--- | --- | ---
GITLAB_ROOT_PASSWORD | string | sets the password for the `root` user on installation
GITLAB_HOST | url | hostname of the GitLab server includes http or https
RAILS_ENV | production/development/staging/test | Rails environment
DATABASE_URL | url | For example: postgresql://localhost/blog_development?pool=5
## Complete database variables
As explained in the [Heroku documentation](https://devcenter.heroku.com/articles/rails-database-connection-behavior) the DATABASE_URL doesn't let you set:
- adapter
- database
- username
- password
- host
- port
To do so please `cp config/database.yml.env config/database.yml` and use the following variables:
Variable | Default
--- | ---
GITLAB_DATABASE_ADAPTER | postgresql
GITLAB_DATABASE_ENCODING | unicode
GITLAB_DATABASE_DATABASE | gitlab_#{ENV['RAILS_ENV']
GITLAB_DATABASE_POOL | 10
GITLAB_DATABASE_USERNAME | root
GITLAB_DATABASE_PASSWORD |
GITLAB_DATABASE_HOST | localhost
GITLAB_DATABASE_PORT | 5432
## Other variables
We welcome merge requests to make more settings configurable via variables.
Please stick to the naming scheme "GITLAB_#{name 1_settings.rb in upper case}".
......@@ -4,8 +4,7 @@
Get all merge requests for this project.
The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. With GitLab 8.2 the return fields `upvotes` and
`downvotes` are deprecated and always return `0`.
The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests.
```
GET /projects/:id/merge_requests
......@@ -58,7 +57,7 @@ Parameters:
## Get single MR
Shows information about a single merge request. With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and always return `0`.
Shows information about a single merge request.
```
GET /projects/:id/merge_request/:merge_request_id
......@@ -141,8 +140,6 @@ Parameters:
## Get single MR changes
Shows information about the merge request including its files and changes.
With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and
always return `0`.
```
GET /projects/:id/merge_request/:merge_request_id/changes
......@@ -213,9 +210,7 @@ Parameters:
## Create MR
Creates a new merge request. With GitLab 8.2 the return fields `upvotes` and `
downvotes` are deprecated and always return `0`.
Creates a new merge request.
```
POST /projects/:id/merge_requests
```
......@@ -266,8 +261,7 @@ If an error occurs, an error number and a message explaining the reason is retur
## Update MR
Updates an existing merge request. You can change the target branch, title, or even close the MR. With GitLab 8.2 the return fields `upvotes` and `downvotes`
are deprecated and always return `0`.
Updates an existing merge request. You can change the target branch, title, or even close the MR.
```
PUT /projects/:id/merge_request/:merge_request_id
......@@ -318,8 +312,7 @@ If an error occurs, an error number and a message explaining the reason is retur
## Accept MR
Merge changes submitted with MR using this API. With GitLab 8.2 the return
fields `upvotes` and `downvotes` are deprecated and always return `0`.
Merge changes submitted with MR using this API.
If merge success you get `200 OK`.
......
......@@ -6,8 +6,7 @@ Notes are comments on snippets, issues or merge requests.
### List project issue notes
Gets a list of all notes for a single issue. With GitLab 8.2 the return fields
`upvote` and `downvote` are deprecated and always return `false`.
Gets a list of all notes for a single issue.
```
GET /projects/:id/issues/:issue_id/notes
......
......@@ -90,7 +90,17 @@ GET /users
You can search for users by email or username with: `/users?search=John`
Also see `def search query` in `app/models/user.rb`.
In addition, you can lookup users by username:
```
GET /users?username=:username
```
For example:
```
GET /users?username=jack_smith
```
## Single user
......
......@@ -2,25 +2,26 @@
### User documentation
+ [Quick Start](quick_start/README.md)
+ [Configuring project (.gitlab-ci.yml)](yaml/README.md)
+ [Configuring runner](runners/README.md)
+ [Configuring deployment](deployment/README.md)
+ [Using Docker Images](docker/using_docker_images.md)
+ [Using Docker Build](docker/using_docker_build.md)
+ [Using Variables](variables/README.md)
+ [Using SSH keys](ssh_keys/README.md)
* [Quick Start](quick_start/README.md)
* [Configuring project (.gitlab-ci.yml)](yaml/README.md)
* [Configuring runner](runners/README.md)
* [Configuring deployment](deployment/README.md)
* [Using Docker Images](docker/using_docker_images.md)
* [Using Docker Build](docker/using_docker_build.md)
* [Using Variables](variables/README.md)
* [Using SSH keys](ssh_keys/README.md)
* [Triggering builds through the API](triggers/README.md)
### Languages
+ [Testing PHP](languages/php.md)
* [Testing PHP](languages/php.md)
### Services
+ [Using MySQL](services/mysql.md)
+ [Using PostgreSQL](services/postgres.md)
+ [Using Redis](services/redis.md)
+ [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
* [Using MySQL](services/mysql.md)
* [Using PostgreSQL](services/postgres.md)
* [Using Redis](services/redis.md)
* [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
### Examples
......@@ -32,5 +33,5 @@
### Administrator documentation
+ [User permissions](permissions/README.md)
+ [API](api/README.md)
* [User permissions](permissions/README.md)
* [API](api/README.md)
# Triggering Builds through the API
_**Note:** This feature was [introduced][ci-229] in GitLab CE 7.14_
Triggers can be used to force a rebuild of a specific branch, tag or commit,
with an API call.
## Add a trigger
You can add a new trigger by going to your project's **Settings > Triggers**.
The **Add trigger** button will create a new token which you can then use to
trigger a rebuild of this particular project.
Every new trigger you create, gets assigned a different token which you can
then use inside your scripts or `.gitlab-ci.yml`. You also have a nice
overview of the time the triggers were last used.
![Triggers page overview](img/triggers_page.png)
## Revoke a trigger
You can revoke a trigger any time by going at your project's
**Settings > Triggers** and hitting the **Revoke** button. The action is
irreversible.
## Trigger a build
To trigger a build you need to send a `POST` request to GitLab's API endpoint:
```
POST /projects/:id/trigger/builds
```
The required parameters are the trigger's `token` and the Git `ref` on which
the trigger will be performed. Valid refs are the branch, the tag or the commit
SHA. The `:id` of a project can be found by [querying the API](../api/projects.md)
or by visiting the **Triggers** page which provides self-explanatory examples.
When a rebuild is triggered, the information is exposed in GitLab's UI under
the **Builds** page and the builds are marked as `triggered`.
![Marked rebuilds as triggered on builds page](img/builds_page.png)
---
You can see which trigger caused the rebuild by visiting the single build page.
The token of the trigger is exposed in the UI as you can see from the image
below.
![Marked rebuilds as triggered on a single build page](img/trigger_single_build.png)
---
See the [Examples](#examples) section for more details on how to actually
trigger a rebuild.
## Pass build variables to a trigger
You can pass any number of arbitrary variables in the trigger API call and they
will be available in GitLab CI so that they can be used in your `.gitlab-ci.yml`
file. The parameter is of the form:
```
variables[key]=value
```
This information is also exposed in the UI.
![Build variables in UI](img/trigger_variables.png)
---
See the [Examples](#examples) section below for more details.
## Examples
Using cURL you can trigger a rebuild with minimal effort, for example:
```bash
curl -X POST \
-F token=TOKEN \
-F ref=master \
https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
In this case, the project with ID `9` will get rebuilt on `master` branch.
### Triggering a build within `.gitlab-ci.yml`
You can also benefit by using triggers in your `.gitlab-ci.yml`. Let's say that
you have two projects, A and B, and you want to trigger a rebuild on the `master`
branch of project B whenever a tag on project A is created. This is the job you
need to add in project's A `.gitlab-ci.yml`:
```yaml
build_docs:
stage: deploy
script:
- "curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds"
only:
- tags
```
Now, whenever a new tag is pushed on project A, the build will run and the
`build_docs` job will be executed, triggering a rebuild of project B. The
`stage: deploy` ensures that this job will run only after all jobs with
`stage: test` complete successfully.
_**Note:** If your project is public, passing the token in plain text is
probably not the wisest idea, so you might want to use a
[secure variable](../variables/README.md#user-defined-variables-secure-variables)
for that purpose._
### Making use of trigger variables
Using trigger variables can be proven useful for a variety of reasons.
* Identifiable jobs. Since the variable is exposed in the UI you can know
why the rebuild was triggered if you pass a variable that explains the
purpose.
* Conditional job processing. You can have conditional jobs that run whenever
a certain variable is present.
Consider the following `.gitlab-ci.yml` where we set three
[stages](../yaml/README.md#stages) and the `upload_package` job is run only
when all jobs from the test and build stages pass. When the `UPLOAD_TO_S3`
variable is non-zero, `make upload` is run.
```yaml
stages:
- test
- build
- package
run_tests:
script:
- make test
build_package:
stage: build
script:
- make build
upload_package:
stage: package
script:
- if [ -n "${UPLOAD_TO_S3}" ]; then make upload; fi
```
You can then trigger a rebuild while you pass the `UPLOAD_TO_S3` variable
and the script of the `upload_package` job will run:
```bash
curl -X POST \
-F token=TOKEN \
-F ref=master \
-F "variables[UPLOAD_TO_S3]=true" \
https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
### Using cron to trigger nightly builds
Whether you craft a script or just run cURL directly, you can trigger builds
in conjunction with cron. The example below triggers a build on the `master`
branch of project with ID `9` every night at `00:30`:
```bash
30 0 * * * curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
......@@ -13,6 +13,7 @@ See the documentation below for details on how to configure these services.
- [Slack](slack.md) Integrate with the Slack chat service
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html).
......
# reCAPTCHA
GitLab leverages [Google's reCAPTCHA](https://www.google.com/recaptcha/intro/index.html)
to protect against spam and abuse. GitLab displays the CAPTCHA form on the sign-up page
to confirm that a real user, not a bot, is attempting to create an account.
## Configuration
To use reCAPTCHA, first you must create a site and private key.
1. Go to the URL: https://www.google.com/recaptcha/admin
2. Fill out the form necessary to obtain reCAPTCHA keys.
3. Login to your GitLab server, with administrator credentials.
4. Go to Applications Settings on Admin Area (`admin/application_settings`)
5. Fill all recaptcha fields with keys from previous steps
6. Check the `Enable reCAPTCHA` checkbox
7. Save the configuration.
......@@ -10,7 +10,7 @@ On public projects the Guest role is not enforced.
All users will be able to create issues, leave comments, and pull or download the project code.
To add or import a user, you can follow the [project users and members
documentation](doc/workflow/add-user/add-user.md).
documentation](../workflow/add-user/add-user.md).
## Project
......
......@@ -6,3 +6,5 @@
- [Information exclusivity](information_exclusivity.md)
- [Reset your root password](reset_root_password.md)
- [User File Uploads](user_file_uploads.md)
- [How we manage the CRIME vulnerability](crime_vulnerability.md)
- [Enforce Two-Factor authentication](two_factor_authentication.md)
# How we manage the TLS protocol CRIME vulnerability
> CRIME ("Compression Ratio Info-leak Made Easy") is a security exploit against
secret web cookies over connections using the HTTPS and SPDY protocols that also
use data compression. When used to recover the content of secret
authentication cookies, it allows an attacker to perform session hijacking on an
authenticated web session, allowing the launching of further attacks.
([CRIME](https://en.wikipedia.org/w/index.php?title=CRIME&oldid=692423806))
### Description
The TLS Protocol CRIME Vulnerability affects compression over HTTPS, therefore
it warns against using SSL Compression (for example gzip) or SPDY which
optionally uses compression as well.
GitLab supports both gzip and [SPDY][ngx-spdy] and mitigates the CRIME
vulnerability by deactivating gzip when HTTPS is enabled. You can see the
sources of the files in question:
* [Source installation NGINX file][source-nginx]
* [Omnibus installation NGINX file][omnibus-nginx]
Although SPDY is enabled in Omnibus installations, CRIME relies on compression
(the 'C') and the default compression level in NGINX's SPDY module is 0
(no compression).
### Nessus
The Nessus scanner, [reports a possible CRIME vulnerability][nessus] in GitLab
similar to the following format:
```
Description
This remote service has one of two configurations that are known to be required for the CRIME attack:
SSL/TLS compression is enabled.
TLS advertises the SPDY protocol earlier than version 4.
...
Output
The following configuration indicates that the remote service may be vulnerable to the CRIME attack:
SPDY support earlier than version 4 is advertised.
```
From the report above it is important to note that Nessus is only checking if
TLS advertises the SPDY protocol earlier than version 4, it does not perform an
attack nor does it check if compression is enabled. With just this approach, it
cannot tell that SPDY's compression is disabled and not subject to the CRIME
vulnerability.
### References
* Nginx ["Module ngx_http_spdy_module"][ngx-spdy]
* Tenable Network Security, Inc. ["Transport Layer Security (TLS) Protocol CRIME Vulnerability"][nessus]
* Wikipedia contributors, ["CRIME"][wiki-crime] Wikipedia, The Free Encyclopedia
[source-nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/gitlab-ssl
[omnibus-nginx]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/templates/default/nginx-gitlab-http.conf.erb
[ngx-spdy]: http://nginx.org/en/docs/http/ngx_http_spdy_module.html
[nessus]: https://www.tenable.com/plugins/index.php?view=single&id=62565
[wiki-crime]: https://en.wikipedia.org/wiki/CRIME
# Enforce Two-factor Authentication (2FA)
Two-factor Authentication (2FA) provides an additional level of security to your
users' GitLab account. Once enabled, in addition to supplying their username and
password to login, they'll be prompted for a code generated by an application on
their phone.
You can read more about it here:
[Two-factor Authentication (2FA)](doc/profile/two_factor_authentication.md)
## Enabling 2FA
Users on GitLab, can enable it without any admin's intervention. If you want to
enforce everyone to setup 2FA, you can choose from two different ways:
1. Enforce on next login
2. Suggest on next login, but allow a grace period before enforcing.
In the Admin area under **Settings** (`/admin/application_settings`), look for
the "Sign-in Restrictions" area, where you can configure both.
If you want 2FA enforcement to take effect on next login, change the grace
period to `0`
## Disabling 2FA for everyone
There may be some special situations where you want to disable 2FA for everyone
even when forced 2FA is disabled. There is a rake task for that:
```
# use this command if you've installed GitLab with the Omnibus package
sudo gitlab-rake gitlab:two_factor:disable_for_all_users
# if you've installed GitLab from source
sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
```
**IMPORTANT: this is a permanent and irreversible action. Users will have to reactivate 2FA from scratch if they want to use it again.**
......@@ -99,8 +99,6 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
# Update init.d script
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
### 7. Update configuration files
......
......@@ -15,15 +15,17 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'I click to emoji in the picker' do
page.within '.emoji-menu' do
page.within '.emoji-menu-content' do
page.first('.emoji-icon').click
end
end
step 'I can remove it by clicking to icon' do
page.within '.awards' do
page.first('.award').click
expect(page).to_not have_selector '.award'
expect do
page.find('.award.active').click
sleep 0.1
end.to change{ page.all(".award").size }.from(3).to(2)
end
end
......@@ -37,7 +39,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I have award added' do
page.within '.awards' do
expect(page).to have_selector '.award'
expect(page.find('.award .counter')).to have_content '1'
expect(page.find('.award.active .counter')).to have_content '1'
end
end
......
......@@ -166,7 +166,6 @@ module API
class MergeRequest < ProjectEntity
expose :target_branch, :source_branch
# deprecated, always returns 0
expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id
......
......@@ -8,11 +8,17 @@ module API
#
# Example Request:
# GET /users
# GET /users?search=Admin
# GET /users?username=root
get do
if params[:username].present?
@users = User.where(username: params[:username])
else
@users = User.all
@users = @users.active if params[:active].present?
@users = @users.search(params[:search]) if params[:search].present?
@users = paginate @users
end
if current_user.is_admin?
present @users, with: Entities::UserFull
......
......@@ -98,7 +98,7 @@ module Banzai
project = project_from_ref(project_ref)
if project && object = find_object(project, id)
title = escape_once(object_link_title(object))
title = object_link_title(object)
klass = reference_class(object_sym)
data = data_attribute(
......@@ -110,17 +110,11 @@ module Banzai
url = matches[:url] if matches.names.include?("url")
url ||= url_for_object(object, project)
text = link_text
unless text
text = object.reference_link_text(context[:project])
extras = object_link_text_extras(object, matches)
text += " (#{extras.join(", ")})" if extras.any?
end
text = link_text || object_link_text(object, matches)
%(<a href="#{url}" #{data}
title="#{title}"
class="#{klass}">#{text}</a>)
title="#{escape_once(title)}"
class="#{klass}">#{escape_once(text)}</a>)
else
match
end
......@@ -140,6 +134,15 @@ module Banzai
def object_link_title(object)
"#{object_class.name.titleize}: #{object.title}"
end
def object_link_text(object, matches)
text = object.reference_link_text(context[:project])
extras = object_link_text_extras(object, matches)
text += " (#{extras.join(", ")})" if extras.any?
text
end
end
end
end
......@@ -63,15 +63,15 @@ module Banzai
url = url_for_issue(id, project, only_path: context[:only_path])
title = escape_once("Issue in #{project.external_issue_tracker.title}")
title = "Issue in #{project.external_issue_tracker.title}"
klass = reference_class(:issue)
data = data_attribute(project: project.id, external_issue: id)
text = link_text || match
%(<a href="#{url}" #{data}
title="#{title}"
class="#{klass}">#{text}</a>)
title="#{escape_once(title)}"
class="#{klass}">#{escape_once(text)}</a>)
end
end
......
......@@ -60,7 +60,7 @@ module Banzai
text = link_text || render_colored_label(label)
%(<a href="#{url}" #{data}
class="#{klass}">#{text}</a>)
class="#{klass}">#{escape_once(text)}</a>)
else
match
end
......
......@@ -11,7 +11,7 @@ module Banzai
class RedactorFilter < HTML::Pipeline::Filter
def call
doc.css('a.gfm').each do |node|
unless user_can_reference?(node)
unless user_can_see_reference?(node)
# The reference should be replaced by the original text,
# which is not always the same as the rendered text.
text = node.attr('data-original') || node.text
......@@ -24,12 +24,12 @@ module Banzai
private
def user_can_reference?(node)
def user_can_see_reference?(node)
if node.has_attribute?('data-reference-filter')
reference_type = node.attr('data-reference-filter')
reference_filter = Banzai::Filter.const_get(reference_type)
reference_filter.user_can_reference?(current_user, node, context)
reference_filter.user_can_see_reference?(current_user, node, context)
else
true
end
......
......@@ -12,7 +12,7 @@ module Banzai
# :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links.
class ReferenceFilter < HTML::Pipeline::Filter
def self.user_can_reference?(user, node, context)
def self.user_can_see_reference?(user, node, context)
if node.has_attribute?('data-project')
project_id = node.attr('data-project').to_i
return true if project_id == context[:project].try(:id)
......@@ -24,6 +24,10 @@ module Banzai
end
end
def self.user_can_reference?(user, node, context)
true
end
def self.referenced_by(node)
raise NotImplementedError, "#{self} does not implement #{__method__}"
end
......@@ -44,11 +48,11 @@ module Banzai
# Returns a String
def data_attribute(attributes = {})
attributes[:reference_filter] = self.class.name.demodulize
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ")
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
end
def escape_once(html)
ERB::Util.html_escape_once(html)
html.html_safe? ? html : ERB::Util.html_escape_once(html)
end
def ignore_parents
......
......@@ -35,7 +35,9 @@ module Banzai
return if context[:reference_filter] && reference_filter != context[:reference_filter]
return unless reference_filter.user_can_reference?(current_user, node, context)
return if author && !reference_filter.user_can_reference?(author, node, context)
return unless reference_filter.user_can_see_reference?(current_user, node, context)
references = reference_filter.referenced_by(node)
return unless references
......@@ -57,6 +59,10 @@ module Banzai
def current_user
context[:current_user]
end
def author
context[:author]
end
end
end
end
......@@ -39,7 +39,7 @@ module Banzai
end
end
def self.user_can_reference?(user, node, context)
def self.user_can_see_reference?(user, node, context)
if node.has_attribute?('data-group')
group = Group.find(node.attr('data-group')) rescue nil
Ability.abilities.allowed?(user, :read_group, group)
......@@ -48,6 +48,18 @@ module Banzai
end
end
def self.user_can_reference?(user, node, context)
# Only team members can reference `@all`
if node.has_attribute?('data-project')
project = Project.find(node.attr('data-project')) rescue nil
return false unless project
user && project.team.member?(user)
else
super
end
end
def call
replace_text_nodes_matching(User.reference_pattern) do |content|
user_link_filter(content)
......@@ -122,7 +134,7 @@ module Banzai
end
def link_tag(url, data, text)
%(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
%(<a href="#{url}" #{data} class="#{link_class}">#{escape_once(text)}</a>)
end
end
end
......
module Gitlab
module Metrics
extend Gitlab::CurrentSettings
RAILS_ROOT = Rails.root.to_s
METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s
PATH_REGEX = /^#{RAILS_ROOT}\/?/
def self.pool_size
current_application_settings[:metrics_pool_size] || 16
end
def self.timeout
current_application_settings[:metrics_timeout] || 10
end
def self.enabled?
current_application_settings[:metrics_enabled] || false
end
def self.mri?
RUBY_ENGINE == 'ruby'
end
def self.method_call_threshold
# This is memoized since this method is called for every instrumented
# method. Loading data from an external cache on every method call slows
# things down too much.
@method_call_threshold ||=
(current_application_settings[:metrics_method_call_threshold] || 10)
end
def self.pool
@pool
end
def self.hostname
@hostname
end
# Returns a relative path and line number based on the last application call
# frame.
def self.last_relative_application_frame
frame = caller_locations.find do |l|
l.path.start_with?(RAILS_ROOT) && !l.path.start_with?(METRICS_ROOT)
end
if frame
return frame.path.sub(PATH_REGEX, ''), frame.lineno
else
return nil, nil
end
end
def self.submit_metrics(metrics)
prepared = prepare_metrics(metrics)
pool.with do |connection|
prepared.each do |metric|
begin
connection.write_points([metric])
rescue StandardError
end
end
end
end
def self.prepare_metrics(metrics)
metrics.map do |hash|
new_hash = hash.symbolize_keys
new_hash[:tags].each do |key, value|
if value.blank?
new_hash[:tags].delete(key)
else
new_hash[:tags][key] = escape_value(value)
end
end
new_hash
end
end
def self.escape_value(value)
value.to_s.gsub('=', '\\=')
end
@hostname = Socket.gethostname
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
if enabled?
@pool = ConnectionPool.new(size: pool_size, timeout: timeout) do
host = current_application_settings[:metrics_host]
user = current_application_settings[:metrics_username]
pw = current_application_settings[:metrics_password]
port = current_application_settings[:metrics_port]
InfluxDB::Client.
new(udp: { host: host, port: port }, username: user, password: pw)
end
end
end
end
module Gitlab
module Metrics
# Class for calculating the difference between two numeric values.
#
# Every call to `compared_with` updates the internal value. This makes it
# possible to use a single Delta instance to calculate the delta over time
# of an ever increasing number.
#
# Example usage:
#
# delta = Delta.new(0)
#
# delta.compared_with(10) # => 10
# delta.compared_with(15) # => 5
# delta.compared_with(20) # => 5
class Delta
def initialize(value = 0)
@value = value
end
# new_value - The value to compare with as a Numeric.
#
# Returns a new Numeric (depending on the type of `new_value`).
def compared_with(new_value)
delta = new_value - @value
@value = new_value
delta
end
end
end
end
module Gitlab
module Metrics
# Module for instrumenting methods.
#
# This module allows instrumenting of methods without having to actually
# alter the target code (e.g. by including modules).
#
# Example usage:
#
# Gitlab::Metrics::Instrumentation.instrument_method(User, :by_login)
module Instrumentation
SERIES = 'method_calls'
def self.configure
yield self
end
# Instruments a class method.
#
# mod - The module to instrument as a Module/Class.
# name - The name of the method to instrument.
def self.instrument_method(mod, name)
instrument(:class, mod, name)
end
# Instruments an instance method.
#
# mod - The module to instrument as a Module/Class.
# name - The name of the method to instrument.
def self.instrument_instance_method(mod, name)
instrument(:instance, mod, name)
end
# Recursively instruments all subclasses of the given root module.
#
# This can be used to for example instrument all ActiveRecord models (as
# these all inherit from ActiveRecord::Base).
#
# This method can optionally take a block to pass to `instrument_methods`
# and `instrument_instance_methods`.
#
# root - The root module for which to instrument subclasses. The root
# module itself is not instrumented.
def self.instrument_class_hierarchy(root, &block)
visit = root.subclasses
until visit.empty?
klass = visit.pop
instrument_methods(klass, &block)
instrument_instance_methods(klass, &block)
klass.subclasses.each { |c| visit << c }
end
end
# Instruments all public methods of a module.
#
# This method optionally takes a block that can be used to determine if a
# method should be instrumented or not. The block is passed the receiving
# module and an UnboundMethod. If the block returns a non truthy value the
# method is not instrumented.
#
# mod - The module to instrument.
def self.instrument_methods(mod)
mod.public_methods(false).each do |name|
method = mod.method(name)
if method.owner == mod.singleton_class
if !block_given? || block_given? && yield(mod, method)
instrument_method(mod, name)
end
end
end
end
# Instruments all public instance methods of a module.
#
# See `instrument_methods` for more information.
#
# mod - The module to instrument.
def self.instrument_instance_methods(mod)
mod.public_instance_methods(false).each do |name|
method = mod.instance_method(name)
if method.owner == mod
if !block_given? || block_given? && yield(mod, method)
instrument_instance_method(mod, name)
end
end
end
end
# Instruments a method.
#
# type - The type (:class or :instance) of method to instrument.
# mod - The module containing the method.
# name - The name of the method to instrument.
def self.instrument(type, mod, name)
return unless Metrics.enabled?
name = name.to_sym
alias_name = :"_original_#{name}"
target = type == :instance ? mod : mod.singleton_class
if type == :instance
target = mod
label = "#{mod.name}##{name}"
else
target = mod.singleton_class
label = "#{mod.name}.#{name}"
end
target.class_eval <<-EOF, __FILE__, __LINE__ + 1
alias_method #{alias_name.inspect}, #{name.inspect}
def #{name}(*args, &block)
trans = Gitlab::Metrics::Instrumentation.transaction
if trans
start = Time.now
retval = __send__(#{alias_name.inspect}, *args, &block)
duration = (Time.now - start) * 1000.0
if duration >= Gitlab::Metrics.method_call_threshold
trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
{ duration: duration },
method: #{label.inspect})
end
retval
else
__send__(#{alias_name.inspect}, *args, &block)
end
end
EOF
end
# Small layer of indirection to make it easier to stub out the current
# transaction.
def self.transaction
Transaction.current
end
end
end
end
module Gitlab
module Metrics
# Class for storing details of a single metric (label, value, etc).
class Metric
attr_reader :series, :values, :tags, :created_at
# series - The name of the series (as a String) to store the metric in.
# values - A Hash containing the values to store.
# tags - A Hash containing extra tags to add to the metrics.
def initialize(series, values, tags = {})
@values = values
@series = series
@tags = tags
@created_at = Time.now.utc
end
# Returns a Hash in a format that can be directly written to InfluxDB.
def to_hash
{
series: @series,
tags: @tags.merge(
hostname: Metrics.hostname,
ruby_engine: RUBY_ENGINE,
ruby_version: RUBY_VERSION,
gitlab_version: Gitlab::VERSION,
process_type: Sidekiq.server? ? 'sidekiq' : 'rails'
),
values: @values,
timestamp: @created_at.to_i * 1_000_000_000
}
end
end
end
end
module Gitlab
module Metrics
# Class for producing SQL queries with sensitive data stripped out.
class ObfuscatedSQL
REPLACEMENT = /
\d+(\.\d+)? # integers, floats
| '.+?' # single quoted strings
| \/.+?(?<!\\)\/ # regexps (including escaped slashes)
/x
MYSQL_REPLACEMENTS = /
".+?" # double quoted strings
/x
# Regex to replace consecutive placeholders with a single one indicating
# the length. This can be useful when a "IN" statement uses thousands of
# IDs (storing this would just be a waste of space).
CONSECUTIVE = /(\?(\s*,\s*)?){2,}/
# sql - The raw SQL query as a String.
def initialize(sql)
@sql = sql
end
# Returns a new, obfuscated SQL query.
def to_s
regex = REPLACEMENT
if Gitlab::Database.mysql?
regex = Regexp.union(regex, MYSQL_REPLACEMENTS)
end
sql = @sql.gsub(regex, '?').gsub(CONSECUTIVE) do |match|
"#{match.count(',') + 1} values"
end
# InfluxDB escapes double quotes upon output, so lets get rid of them
# whenever we can.
if Gitlab::Database.postgresql?
sql = sql.delete('"')
end
sql.tr("\n", ' ')
end
end
end
end
module Gitlab
module Metrics
# Rack middleware for tracking Rails requests.
class RackMiddleware
CONTROLLER_KEY = 'action_controller.instance'
def initialize(app)
@app = app
end
# env - A Hash containing Rack environment details.
def call(env)
trans = transaction_from_env(env)
retval = nil
begin
retval = trans.run { @app.call(env) }
# Even in the event of an error we want to submit any metrics we
# might've gathered up to this point.
ensure
if env[CONTROLLER_KEY]
tag_controller(trans, env)
end
trans.finish
end
retval
end
def transaction_from_env(env)
trans = Transaction.new
trans.add_tag(:request_method, env['REQUEST_METHOD'])
trans.add_tag(:request_uri, env['REQUEST_URI'])
trans
end
def tag_controller(trans, env)
controller = env[CONTROLLER_KEY]
label = "#{controller.class.name}##{controller.action_name}"
trans.add_tag(:action, label)
end
end
end
end
module Gitlab
module Metrics
# Class that sends certain metrics to InfluxDB at a specific interval.
#
# This class is used to gather statistics that can't be directly associated
# with a transaction such as system memory usage, garbage collection
# statistics, etc.
class Sampler
# interval - The sampling interval in seconds.
def initialize(interval = 15)
@interval = interval
@metrics = []
@last_minor_gc = Delta.new(GC.stat[:minor_gc_count])
@last_major_gc = Delta.new(GC.stat[:major_gc_count])
if Gitlab::Metrics.mri?
require 'allocations'
Allocations.start
end
end
def start
Thread.new do
Thread.current.abort_on_exception = true
loop do
sleep(@interval)
sample
end
end
end
def sample
sample_memory_usage
sample_file_descriptors
sample_objects
sample_gc
flush
ensure
GC::Profiler.clear
@metrics.clear
end
def flush
Metrics.submit_metrics(@metrics.map(&:to_hash))
end
def sample_memory_usage
@metrics << Metric.new('memory_usage', value: System.memory_usage)
end
def sample_file_descriptors
@metrics << Metric.
new('file_descriptors', value: System.file_descriptor_count)
end
if Metrics.mri?
def sample_objects
sample = Allocations.to_hash
counts = sample.each_with_object({}) do |(klass, count), hash|
hash[klass.name] = count
end
# Symbols aren't allocated so we'll need to add those manually.
counts['Symbol'] = Symbol.all_symbols.length
counts.each do |name, count|
@metrics << Metric.new('object_counts', { count: count }, type: name)
end
end
else
def sample_objects
end
end
def sample_gc
time = GC::Profiler.total_time * 1000.0
stats = GC.stat.merge(total_time: time)
# We want the difference of GC runs compared to the last sample, not the
# total amount since the process started.
stats[:minor_gc_count] =
@last_minor_gc.compared_with(stats[:minor_gc_count])
stats[:major_gc_count] =
@last_major_gc.compared_with(stats[:major_gc_count])
stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count]
@metrics << Metric.new('gc_statistics', stats)
end
end
end
end
module Gitlab
module Metrics
# Sidekiq middleware for tracking jobs.
#
# This middleware is intended to be used as a server-side middleware.
class SidekiqMiddleware
def call(worker, message, queue)
trans = Transaction.new
begin
trans.run { yield }
ensure
tag_worker(trans, worker)
trans.finish
end
end
def tag_worker(trans, worker)
trans.add_tag(:action, "#{worker.class.name}#perform")
end
end
end
end
module Gitlab
module Metrics
module Subscribers
# Class for tracking the rendering timings of views.
class ActionView < ActiveSupport::Subscriber
attach_to :action_view
SERIES = 'views'
def render_template(event)
track(event) if current_transaction
end
alias_method :render_view, :render_template
private
def track(event)
values = values_for(event)
tags = tags_for(event)
current_transaction.add_metric(SERIES, values, tags)
end
def relative_path(path)
path.gsub(/^#{Rails.root.to_s}\/?/, '')
end
def values_for(event)
{ duration: event.duration }
end
def tags_for(event)
path = relative_path(event.payload[:identifier])
tags = { view: path }
file, line = Metrics.last_relative_application_frame
if file and line
tags[:file] = file
tags[:line] = line
end
tags
end
def current_transaction
Transaction.current
end
end
end
end
end
module Gitlab
module Metrics
module Subscribers
# Class for tracking raw SQL queries.
#
# Queries are obfuscated before being logged to ensure no private data is
# exposed via InfluxDB/Grafana.
class ActiveRecord < ActiveSupport::Subscriber
attach_to :active_record
SERIES = 'sql_queries'
def sql(event)
return unless current_transaction
values = values_for(event)
tags = tags_for(event)
current_transaction.add_metric(SERIES, values, tags)
end
private
def values_for(event)
{ duration: event.duration }
end
def tags_for(event)
sql = ObfuscatedSQL.new(event.payload[:sql]).to_s
tags = { sql: sql }
file, line = Metrics.last_relative_application_frame
if file and line
tags[:file] = file
tags[:line] = line
end
tags
end
def current_transaction
Transaction.current
end
end
end
end
end
module Gitlab
module Metrics
# Module for gathering system/process statistics such as the memory usage.
#
# This module relies on the /proc filesystem being available. If /proc is
# not available the methods of this module will be stubbed.
module System
if File.exist?('/proc')
# Returns the current process' memory usage in bytes.
def self.memory_usage
mem = 0
match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/)
if match and match[1]
mem = match[1].to_f * 1024
end
mem
end
def self.file_descriptor_count
Dir.glob('/proc/self/fd/*').length
end
else
def self.memory_usage
0.0
end
def self.file_descriptor_count
0
end
end
end
end
end
module Gitlab
module Metrics
# Class for storing metrics information of a single transaction.
class Transaction
THREAD_KEY = :_gitlab_metrics_transaction
SERIES = 'transactions'
attr_reader :uuid, :tags
def self.current
Thread.current[THREAD_KEY]
end
# name - The name of this transaction as a String.
def initialize
@metrics = []
@uuid = SecureRandom.uuid
@started_at = nil
@finished_at = nil
@tags = {}
end
def duration
@finished_at ? (@finished_at - @started_at) * 1000.0 : 0.0
end
def run
Thread.current[THREAD_KEY] = self
@started_at = Time.now
yield
ensure
@finished_at = Time.now
Thread.current[THREAD_KEY] = nil
end
def add_metric(series, values, tags = {})
tags = tags.merge(transaction_id: @uuid)
@metrics << Metric.new(series, values, tags)
end
def add_tag(key, value)
@tags[key] = value
end
def finish
track_self
submit
end
def track_self
add_metric(SERIES, { duration: duration }, @tags)
end
def submit
Metrics.submit_metrics(@metrics.map(&:to_hash))
end
end
end
end
module Gitlab
module Recaptcha
def self.load_configurations!
if current_application_settings.recaptcha_enabled
::Recaptcha.configure do |config|
config.public_key = current_application_settings.recaptcha_site_key
config.private_key = current_application_settings.recaptcha_private_key
end
true
end
end
end
end
......@@ -3,11 +3,12 @@ require 'banzai'
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
attr_accessor :project, :current_user
attr_accessor :project, :current_user, :author
def initialize(project, current_user = nil)
def initialize(project, current_user = nil, author = nil)
@project = project
@current_user = current_user
@author = author
@references = {}
......@@ -20,18 +21,22 @@ module Gitlab
%i(user label merge_request snippet commit commit_range).each do |type|
define_method("#{type}s") do
@references[type] ||= references(type, project: project, current_user: current_user)
@references[type] ||= references(type, reference_context)
end
end
def issues
options = { project: project, current_user: current_user }
if project && project.jira_tracker?
@references[:external_issue] ||= references(:external_issue, options)
@references[:external_issue] ||= references(:external_issue, reference_context)
else
@references[:issue] ||= references(:issue, options)
@references[:issue] ||= references(:issue, reference_context)
end
end
private
def reference_context
{ project: project, current_user: current_user, author: author }
end
end
end
......@@ -98,4 +98,56 @@ feature 'Login', feature: true do
expect(page).to have_content('Invalid login or password.')
end
end
describe 'with required two-factor authentication enabled' do
let(:user) { create(:user) }
before(:each) { stub_application_setting(require_two_factor_authentication: true) }
context 'with grace period defined' do
before(:each) do
stub_application_setting(two_factor_grace_period: 48)
login_with(user)
end
context 'within the grace period' do
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).to have_content('You must configure Two-Factor Authentication in your account until')
end
it 'two-factor configuration is skippable' do
expect(current_path).to eq new_profile_two_factor_auth_path
click_link 'Configure it later'
expect(current_path).to eq root_path
end
end
context 'after the grace period' do
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).to have_content('You must configure Two-Factor Authentication in your account.')
end
it 'two-factor configuration is not skippable' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
end
end
context 'without grace pariod defined' do
before(:each) do
stub_application_setting(two_factor_grace_period: 0)
login_with(user)
end
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).to have_content('You must configure Two-Factor Authentication in your account.')
end
end
end
end
......@@ -141,4 +141,11 @@ describe IssuesHelper do
expect(note_active_class(Note.all, @note.author)).to eq("active")
end
end
describe "#awards_sort" do
it "sorts a hash so thumbsup and thumbsdown are always on top" do
data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" }
expect(awards_sort(data).keys).to eq(["thumbsup", "thumbsdown", "lifter"])
end
end
end
require 'rails_helper'
describe PageLayoutHelper do
describe 'page_description' do
it 'defaults to value returned by page_description_default helper' do
allow(helper).to receive(:page_description_default).and_return('Foo')
expect(helper.page_description).to eq 'Foo'
end
it 'returns the last-pushed description' do
helper.page_description('Foo')
helper.page_description('Bar')
helper.page_description('Baz')
expect(helper.page_description).to eq 'Baz'
end
it 'squishes multiple newlines' do
helper.page_description("Foo\nBar\nBaz")
expect(helper.page_description).to eq 'Foo Bar Baz'
end
it 'truncates' do
helper.page_description <<-LOREM.strip_heredoc
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo
ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis
dis parturient montes, nascetur ridiculus mus. Donec quam felis,
ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa
quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget,
arcu.
LOREM
expect(helper.page_description).to end_with 'quam felis,...'
end
it 'sanitizes all HTML' do
helper.page_description("<b>Bold</b> <h1>Header</h1>")
expect(helper.page_description).to eq 'Bold Header'
end
end
describe 'page_description_default' do
it 'uses Project description when available' do
project = double(description: 'Project Description')
helper.instance_variable_set(:@project, project)
expect(helper.page_description_default).to eq 'Project Description'
end
it 'uses brand_title when Project description is nil' do
project = double(description: nil)
helper.instance_variable_set(:@project, project)
expect(helper).to receive(:brand_title).and_return('Brand Title')
expect(helper.page_description_default).to eq 'Brand Title'
end
it 'falls back to brand_title' do
allow(helper).to receive(:brand_title).and_return('Brand Title')
expect(helper.page_description_default).to eq 'Brand Title'
end
end
describe 'page_image' do
it 'defaults to the GitLab logo' do
expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
end
context 'with @project' do
it 'uses Project avatar if available' do
project = double(avatar_url: 'http://example.com/uploads/avatar.png')
helper.instance_variable_set(:@project, project)
expect(helper.page_image).to eq project.avatar_url
end
it 'falls back to the default' do
project = double(avatar_url: nil)
helper.instance_variable_set(:@project, project)
expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
end
end
context 'with @user' do
it 'delegates to avatar_icon helper' do
user = double('User')
helper.instance_variable_set(:@user, user)
expect(helper).to receive(:avatar_icon).with(user)
helper.page_image
end
end
end
describe 'page_card_attributes' do
it 'raises ArgumentError when given more than two attributes' do
map = { foo: 'foo', bar: 'bar', baz: 'baz' }
expect { helper.page_card_attributes(map) }.
to raise_error(ArgumentError, /more than two attributes/)
end
it 'rejects blank values' do
map = { foo: 'foo', bar: '' }
helper.page_card_attributes(map)
expect(helper.page_card_attributes).to eq({ foo: 'foo' })
end
end
describe 'page_card_meta_tags' do
it 'returns the twitter:label and twitter:data tags' do
allow(helper).to receive(:page_card_attributes).and_return(foo: 'bar')
tags = helper.page_card_meta_tags
aggregate_failures do
expect(tags).to include %q(<meta property="twitter:label1" content="foo" />)
expect(tags).to include %q(<meta property="twitter:data1" content="bar" />)
end
end
end
end
%a.btn-close
:css
.hidden { display: none !important; }
.flash-container
.flash-alert
.flash-notice
.status-box.status-box-open Open
.status-box.status-box-closed.hidden Closed
%a.btn-close{"href" => "http://gitlab.com/issues/6/close"} Close
%a.btn-reopen.hidden{"href" => "http://gitlab.com/issues/6/reopen"} Reopen
.detail-page-description
.description.js-task-list-container
......
......@@ -20,3 +20,89 @@ describe 'Issue', ->
expect(req.data.issue.description).not.toBe(null)
$('.js-task-list-field').trigger('tasklist:changed')
describe 'reopen/close issue', ->
fixture.preload('issues_show.html')
beforeEach ->
fixture.load('issues_show.html')
@issue = new Issue()
it 'closes an issue', ->
$.ajax = (obj) ->
expect(obj.type).toBe('PUT')
expect(obj.url).toBe('http://gitlab.com/issues/6/close')
obj.success saved: true
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
expect($btnReopen).toBeHidden()
expect($btnClose.text()).toBe('Close')
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
expect($btnReopen).toBeVisible()
expect($btnClose).toBeHidden()
expect($('div.status-box-closed')).toBeVisible()
expect($('div.status-box-open')).toBeHidden()
it 'fails to closes an issue with success:false', ->
$.ajax = (obj) ->
expect(obj.type).toBe('PUT')
expect(obj.url).toBe('http://goesnowhere.nothing/whereami')
obj.success saved: false
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
$btnClose.attr('href','http://goesnowhere.nothing/whereami')
expect($btnReopen).toBeHidden()
expect($btnClose.text()).toBe('Close')
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-closed')).toBeHidden()
expect($('div.status-box-open')).toBeVisible()
expect($('div.flash-alert')).toBeVisible()
expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
it 'fails to closes an issue with HTTP error', ->
$.ajax = (obj) ->
expect(obj.type).toBe('PUT')
expect(obj.url).toBe('http://goesnowhere.nothing/whereami')
obj.error()
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
$btnClose.attr('href','http://goesnowhere.nothing/whereami')
expect($btnReopen).toBeHidden()
expect($btnClose.text()).toBe('Close')
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-closed')).toBeHidden()
expect($('div.status-box-open')).toBeVisible()
expect($('div.flash-alert')).toBeVisible()
expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
it 'reopens an issue', ->
$.ajax = (obj) ->
expect(obj.type).toBe('PUT')
expect(obj.url).toBe('http://gitlab.com/issues/6/reopen')
obj.success saved: true
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
expect($btnReopen.text()).toBe('Reopen')
$btnReopen.trigger('click')
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-open')).toBeVisible()
expect($('div.status-box-closed')).toBeHidden()
\ No newline at end of file
......@@ -37,12 +37,25 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
.to eq urls.namespace_project_url(project.namespace, project)
end
context "when the author is a member of the project" do
it 'adds to the results hash' do
result = reference_pipeline_result("Hey #{reference}")
result = reference_pipeline_result("Hey #{reference}", author: project.creator)
expect(result[:references][:user]).to eq [project.creator]
end
end
context "when the author is not a member of the project" do
let(:other_user) { create(:user) }
it "doesn't add to the results hash" do
result = reference_pipeline_result("Hey #{reference}", author: other_user)
expect(result[:references][:user]).to eq []
end
end
end
context 'mentioning a user' do
it 'links to a User' do
doc = reference_filter("Hey #{reference}")
......
......@@ -517,7 +517,7 @@ module Ci
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
end
it "returns errors if there is no any jobs defined" do
it "returns errors if there are no jobs defined" do
config = YAML.dump({ before_script: ["bundle update"] })
expect do
GitlabCiYamlProcessor.new(config, path)
......
require 'spec_helper'
describe Gitlab::Metrics::Delta do
let(:delta) { described_class.new }
describe '#compared_with' do
it 'returns the delta as a Numeric' do
expect(delta.compared_with(5)).to eq(5)
end
it 'bases the delta on a previously used value' do
expect(delta.compared_with(5)).to eq(5)
expect(delta.compared_with(15)).to eq(10)
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Instrumentation do
let(:transaction) { Gitlab::Metrics::Transaction.new }
before do
@dummy = Class.new do
def self.foo(text = 'foo')
text
end
def bar(text = 'bar')
text
end
end
allow(@dummy).to receive(:name).and_return('Dummy')
end
describe '.configure' do
it 'yields self' do
described_class.configure do |c|
expect(c).to eq(described_class)
end
end
end
describe '.instrument_method' do
describe 'with metrics enabled' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
described_class.instrument_method(@dummy, :foo)
end
it 'renames the original method' do
expect(@dummy).to respond_to(:_original_foo)
end
it 'calls the instrumented method with the correct arguments' do
expect(@dummy.foo).to eq('foo')
end
it 'tracks the call duration upon calling the method' do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(0)
allow(described_class).to receive(:transaction).
and_return(transaction)
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, an_instance_of(Hash),
method: 'Dummy.foo')
@dummy.foo
end
it 'does not track method calls below a given duration threshold' do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(100)
expect(transaction).to_not receive(:add_metric)
@dummy.foo
end
end
describe 'with metrics disabled' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(false)
end
it 'does not instrument the method' do
described_class.instrument_method(@dummy, :foo)
expect(@dummy).to_not respond_to(:_original_foo)
end
end
end
describe '.instrument_instance_method' do
describe 'with metrics enabled' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
described_class.
instrument_instance_method(@dummy, :bar)
end
it 'renames the original method' do
expect(@dummy.method_defined?(:_original_bar)).to eq(true)
end
it 'calls the instrumented method with the correct arguments' do
expect(@dummy.new.bar).to eq('bar')
end
it 'tracks the call duration upon calling the method' do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(0)
allow(described_class).to receive(:transaction).
and_return(transaction)
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, an_instance_of(Hash),
method: 'Dummy#bar')
@dummy.new.bar
end
it 'does not track method calls below a given duration threshold' do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(100)
expect(transaction).to_not receive(:add_metric)
@dummy.new.bar
end
end
describe 'with metrics disabled' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(false)
end
it 'does not instrument the method' do
described_class.
instrument_instance_method(@dummy, :bar)
expect(@dummy.method_defined?(:_original_bar)).to eq(false)
end
end
end
describe '.instrument_class_hierarchy' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
@child1 = Class.new(@dummy) do
def self.child1_foo; end
def child1_bar; end
end
@child2 = Class.new(@child1) do
def self.child2_foo; end
def child2_bar; end
end
end
it 'recursively instruments a class hierarchy' do
described_class.instrument_class_hierarchy(@dummy)
expect(@child1).to respond_to(:_original_child1_foo)
expect(@child2).to respond_to(:_original_child2_foo)
expect(@child1.method_defined?(:_original_child1_bar)).to eq(true)
expect(@child2.method_defined?(:_original_child2_bar)).to eq(true)
end
it 'does not instrument the root module' do
described_class.instrument_class_hierarchy(@dummy)
expect(@dummy).to_not respond_to(:_original_foo)
expect(@dummy.method_defined?(:_original_bar)).to eq(false)
end
end
describe '.instrument_methods' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
end
it 'instruments all public class methods' do
described_class.instrument_methods(@dummy)
expect(@dummy).to respond_to(:_original_foo)
end
it 'only instruments methods directly defined in the module' do
mod = Module.new do
def kittens
end
end
@dummy.extend(mod)
described_class.instrument_methods(@dummy)
expect(@dummy).to_not respond_to(:_original_kittens)
end
it 'can take a block to determine if a method should be instrumented' do
described_class.instrument_methods(@dummy) do
false
end
expect(@dummy).to_not respond_to(:_original_foo)
end
end
describe '.instrument_instance_methods' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
end
it 'instruments all public instance methods' do
described_class.instrument_instance_methods(@dummy)
expect(@dummy.method_defined?(:_original_bar)).to eq(true)
end
it 'only instruments methods directly defined in the module' do
mod = Module.new do
def kittens
end
end
@dummy.include(mod)
described_class.instrument_instance_methods(@dummy)
expect(@dummy.method_defined?(:_original_kittens)).to eq(false)
end
it 'can take a block to determine if a method should be instrumented' do
described_class.instrument_instance_methods(@dummy) do
false
end
expect(@dummy.method_defined?(:_original_bar)).to eq(false)
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Metric do
let(:metric) do
described_class.new('foo', { number: 10 }, { host: 'localtoast' })
end
describe '#series' do
subject { metric.series }
it { is_expected.to eq('foo') }
end
describe '#values' do
subject { metric.values }
it { is_expected.to eq({ number: 10 }) }
end
describe '#tags' do
subject { metric.tags }
it { is_expected.to eq({ host: 'localtoast' }) }
end
describe '#to_hash' do
it 'returns a Hash' do
expect(metric.to_hash).to be_an_instance_of(Hash)
end
describe 'the returned Hash' do
let(:hash) { metric.to_hash }
it 'includes the series' do
expect(hash[:series]).to eq('foo')
end
it 'includes the tags' do
expect(hash[:tags]).to be_an_instance_of(Hash)
expect(hash[:tags][:hostname]).to be_an_instance_of(String)
expect(hash[:tags][:ruby_engine]).to be_an_instance_of(String)
expect(hash[:tags][:ruby_version]).to be_an_instance_of(String)
expect(hash[:tags][:gitlab_version]).to be_an_instance_of(String)
expect(hash[:tags][:process_type]).to be_an_instance_of(String)
end
it 'includes the values' do
expect(hash[:values]).to eq({ number: 10 })
end
it 'includes the timestamp' do
expect(hash[:timestamp]).to be_an_instance_of(Fixnum)
end
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::ObfuscatedSQL do
describe '#to_s' do
it 'replaces newlines with a space' do
sql = described_class.new("SELECT x\nFROM y")
expect(sql.to_s).to eq('SELECT x FROM y')
end
describe 'using single values' do
it 'replaces a single integer' do
sql = described_class.new('SELECT x FROM y WHERE a = 10')
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
it 'replaces a single float' do
sql = described_class.new('SELECT x FROM y WHERE a = 10.5')
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
it 'replaces a single quoted string' do
sql = described_class.new("SELECT x FROM y WHERE a = 'foo'")
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
if Gitlab::Database.mysql?
it 'replaces a double quoted string' do
sql = described_class.new('SELECT x FROM y WHERE a = "foo"')
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
end
it 'replaces a single regular expression' do
sql = described_class.new('SELECT x FROM y WHERE a = /foo/')
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
it 'replaces regular expressions using escaped slashes' do
sql = described_class.new('SELECT x FROM y WHERE a = /foo\/bar/')
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
end
describe 'using consecutive values' do
it 'replaces multiple integers' do
sql = described_class.new('SELECT x FROM y WHERE z IN (10, 20, 30)')
expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)')
end
it 'replaces multiple floats' do
sql = described_class.new('SELECT x FROM y WHERE z IN (1.5, 2.5, 3.5)')
expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)')
end
it 'replaces multiple single quoted strings' do
sql = described_class.new("SELECT x FROM y WHERE z IN ('foo', 'bar')")
expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
end
if Gitlab::Database.mysql?
it 'replaces multiple double quoted strings' do
sql = described_class.new('SELECT x FROM y WHERE z IN ("foo", "bar")')
expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
end
end
it 'replaces multiple regular expressions' do
sql = described_class.new('SELECT x FROM y WHERE z IN (/foo/, /bar/)')
expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
end
end
if Gitlab::Database.postgresql?
it 'replaces double quotes' do
sql = described_class.new('SELECT "x" FROM "y"')
expect(sql.to_s).to eq('SELECT x FROM y')
end
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::RackMiddleware do
let(:app) { double(:app) }
let(:middleware) { described_class.new(app) }
let(:env) { { 'REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/foo' } }
describe '#call' do
before do
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
end
it 'tracks a transaction' do
expect(app).to receive(:call).with(env).and_return('yay')
expect(middleware.call(env)).to eq('yay')
end
it 'tags a transaction with the name and action of a controller' do
klass = double(:klass, name: 'TestController')
controller = double(:controller, class: klass, action_name: 'show')
env['action_controller.instance'] = controller
allow(app).to receive(:call).with(env)
expect(middleware).to receive(:tag_controller).
with(an_instance_of(Gitlab::Metrics::Transaction), env)
middleware.call(env)
end
end
describe '#transaction_from_env' do
let(:transaction) { middleware.transaction_from_env(env) }
it 'returns a Transaction' do
expect(transaction).to be_an_instance_of(Gitlab::Metrics::Transaction)
end
it 'tags the transaction with the request method and URI' do
expect(transaction.tags[:request_method]).to eq('GET')
expect(transaction.tags[:request_uri]).to eq('/foo')
end
end
describe '#tag_controller' do
let(:transaction) { middleware.transaction_from_env(env) }
it 'tags a transaction with the name and action of a controller' do
klass = double(:klass, name: 'TestController')
controller = double(:controller, class: klass, action_name: 'show')
env['action_controller.instance'] = controller
middleware.tag_controller(transaction, env)
expect(transaction.tags[:action]).to eq('TestController#show')
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Sampler do
let(:sampler) { described_class.new(5) }
after do
Allocations.stop if Gitlab::Metrics.mri?
end
describe '#start' do
it 'gathers a sample at a given interval' do
expect(sampler).to receive(:sleep).with(5)
expect(sampler).to receive(:sample)
expect(sampler).to receive(:loop).and_yield
sampler.start.join
end
end
describe '#sample' do
it 'samples various statistics' do
expect(sampler).to receive(:sample_memory_usage)
expect(sampler).to receive(:sample_file_descriptors)
expect(sampler).to receive(:sample_objects)
expect(sampler).to receive(:sample_gc)
expect(sampler).to receive(:flush)
sampler.sample
end
it 'clears any GC profiles' do
expect(sampler).to receive(:flush)
expect(GC::Profiler).to receive(:clear)
sampler.sample
end
end
describe '#flush' do
it 'schedules the metrics using Sidekiq' do
expect(Gitlab::Metrics).to receive(:submit_metrics).
with([an_instance_of(Hash)])
sampler.sample_memory_usage
sampler.flush
end
end
describe '#sample_memory_usage' do
it 'adds a metric containing the memory usage' do
expect(Gitlab::Metrics::System).to receive(:memory_usage).
and_return(9000)
expect(Gitlab::Metrics::Metric).to receive(:new).
with('memory_usage', value: 9000).
and_call_original
sampler.sample_memory_usage
end
end
describe '#sample_file_descriptors' do
it 'adds a metric containing the amount of open file descriptors' do
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count).
and_return(4)
expect(Gitlab::Metrics::Metric).to receive(:new).
with('file_descriptors', value: 4).
and_call_original
sampler.sample_file_descriptors
end
end
describe '#sample_objects' do
it 'adds a metric containing the amount of allocated objects' do
expect(Gitlab::Metrics::Metric).to receive(:new).
with('object_counts', an_instance_of(Hash), an_instance_of(Hash)).
at_least(:once).
and_call_original
sampler.sample_objects
end
end
describe '#sample_gc' do
it 'adds a metric containing garbage collection statistics' do
expect(GC::Profiler).to receive(:total_time).and_return(0.24)
expect(Gitlab::Metrics::Metric).to receive(:new).
with('gc_statistics', an_instance_of(Hash)).
and_call_original
sampler.sample_gc
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::SidekiqMiddleware do
let(:middleware) { described_class.new }
describe '#call' do
it 'tracks the transaction' do
worker = Class.new.new
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
middleware.call(worker, 'test', :test) { nil }
end
end
describe '#tag_worker' do
it 'adds the worker class and action to the transaction' do
trans = Gitlab::Metrics::Transaction.new
worker = double(:worker, class: double(:class, name: 'TestWorker'))
expect(trans).to receive(:add_tag).with(:action, 'TestWorker#perform')
middleware.tag_worker(trans, worker)
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Subscribers::ActionView do
let(:transaction) { Gitlab::Metrics::Transaction.new }
let(:subscriber) { described_class.new }
let(:event) do
root = Rails.root.to_s
double(:event, duration: 2.1,
payload: { identifier: "#{root}/app/views/x.html.haml" })
end
before do
allow(subscriber).to receive(:current_transaction).and_return(transaction)
allow(Gitlab::Metrics).to receive(:last_relative_application_frame).
and_return(['app/views/x.html.haml', 4])
end
describe '#render_template' do
it 'tracks rendering of a template' do
values = { duration: 2.1 }
tags = {
view: 'app/views/x.html.haml',
file: 'app/views/x.html.haml',
line: 4
}
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, values, tags)
subscriber.render_template(event)
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Subscribers::ActiveRecord do
let(:transaction) { Gitlab::Metrics::Transaction.new }
let(:subscriber) { described_class.new }
let(:event) do
double(:event, duration: 0.2,
payload: { sql: 'SELECT * FROM users WHERE id = 10' })
end
before do
allow(subscriber).to receive(:current_transaction).and_return(transaction)
allow(Gitlab::Metrics).to receive(:last_relative_application_frame).
and_return(['app/models/foo.rb', 4])
end
describe '#sql' do
it 'tracks the execution of a SQL query' do
sql = 'SELECT * FROM users WHERE id = ?'
values = { duration: 0.2 }
tags = { sql: sql, file: 'app/models/foo.rb', line: 4 }
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, values, tags)
subscriber.sql(event)
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::System do
if File.exist?('/proc')
describe '.memory_usage' do
it "returns the process' memory usage in bytes" do
expect(described_class.memory_usage).to be > 0
end
end
describe '.file_descriptor_count' do
it 'returns the amount of open file descriptors' do
expect(described_class.file_descriptor_count).to be > 0
end
end
else
describe '.memory_usage' do
it 'returns 0.0' do
expect(described_class.memory_usage).to eq(0.0)
end
end
describe '.file_descriptor_count' do
it 'returns 0' do
expect(described_class.file_descriptor_count).to eq(0)
end
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Transaction do
let(:transaction) { described_class.new }
describe '#duration' do
it 'returns the duration of a transaction in seconds' do
transaction.run { sleep(0.5) }
expect(transaction.duration).to be >= 0.5
end
end
describe '#run' do
it 'yields the supplied block' do
expect { |b| transaction.run(&b) }.to yield_control
end
it 'stores the transaction in the current thread' do
transaction.run do
expect(Thread.current[described_class::THREAD_KEY]).to eq(transaction)
end
end
it 'removes the transaction from the current thread upon completion' do
transaction.run { }
expect(Thread.current[described_class::THREAD_KEY]).to be_nil
end
end
describe '#add_metric' do
it 'adds a metric tagged with the transaction UUID' do
expect(Gitlab::Metrics::Metric).to receive(:new).
with('foo', { number: 10 }, { transaction_id: transaction.uuid })
transaction.add_metric('foo', number: 10)
end
end
describe '#add_tag' do
it 'adds a tag' do
transaction.add_tag(:foo, 'bar')
expect(transaction.tags).to eq({ foo: 'bar' })
end
end
describe '#finish' do
it 'tracks the transaction details and submits them to Sidekiq' do
expect(transaction).to receive(:track_self)
expect(transaction).to receive(:submit)
transaction.finish
end
end
describe '#track_self' do
it 'adds a metric for the transaction itself' do
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, { duration: transaction.duration }, {})
transaction.track_self
end
end
describe '#submit' do
it 'submits the metrics to Sidekiq' do
transaction.track_self
expect(Gitlab::Metrics).to receive(:submit_metrics).
with([an_instance_of(Hash)])
transaction.submit
end
end
end
require 'spec_helper'
describe Gitlab::Metrics do
describe '.pool_size' do
it 'returns a Fixnum' do
expect(described_class.pool_size).to be_an_instance_of(Fixnum)
end
end
describe '.timeout' do
it 'returns a Fixnum' do
expect(described_class.timeout).to be_an_instance_of(Fixnum)
end
end
describe '.enabled?' do
it 'returns a boolean' do
expect([true, false].include?(described_class.enabled?)).to eq(true)
end
end
describe '.hostname' do
it 'returns a String containing the hostname' do
expect(described_class.hostname).to eq(Socket.gethostname)
end
end
describe '.last_relative_application_frame' do
it 'returns an Array containing a file path and line number' do
file, line = described_class.last_relative_application_frame
expect(line).to eq(__LINE__ - 2)
expect(file).to eq('spec/lib/gitlab/metrics_spec.rb')
end
end
describe '#submit_metrics' do
it 'prepares and writes the metrics to InfluxDB' do
connection = double(:connection)
pool = double(:pool)
expect(pool).to receive(:with).and_yield(connection)
expect(connection).to receive(:write_points).with(an_instance_of(Array))
expect(Gitlab::Metrics).to receive(:pool).and_return(pool)
described_class.submit_metrics([{ 'series' => 'kittens', 'tags' => {} }])
end
end
describe '#prepare_metrics' do
it 'returns a Hash with the keys as Symbols' do
metrics = described_class.
prepare_metrics([{ 'values' => {}, 'tags' => {} }])
expect(metrics).to eq([{ values: {}, tags: {} }])
end
it 'escapes tag values' do
metrics = described_class.prepare_metrics([
{ 'values' => {}, 'tags' => { 'foo' => 'bar=' } }
])
expect(metrics).to eq([{ values: {}, tags: { 'foo' => 'bar\\=' } }])
end
it 'drops empty tags' do
metrics = described_class.prepare_metrics([
{ 'values' => {}, 'tags' => { 'cats' => '', 'dogs' => nil } }
])
expect(metrics).to eq([{ values: {}, tags: {} }])
end
end
describe '#escape_value' do
it 'escapes an equals sign' do
expect(described_class.escape_value('foo=')).to eq('foo\\=')
end
it 'casts values to Strings' do
expect(described_class.escape_value(10)).to eq('10')
end
end
end
......@@ -27,6 +27,7 @@
# admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null
# runners_registration_token :string(255)
#
require 'spec_helper'
......
......@@ -81,4 +81,36 @@ describe Issue, "Issuable" do
expect(hook_data[:object_attributes]).to eq(issue.hook_attrs)
end
end
describe '#card_attributes' do
it 'includes the author name' do
allow(issue).to receive(:author).and_return(double(name: 'Robert'))
allow(issue).to receive(:assignee).and_return(nil)
expect(issue.card_attributes).
to eq({ 'Author' => 'Robert', 'Assignee' => nil })
end
it 'includes the assignee name' do
allow(issue).to receive(:author).and_return(double(name: 'Robert'))
allow(issue).to receive(:assignee).and_return(double(name: 'Douwe'))
expect(issue.card_attributes).
to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
end
end
describe "votes" do
before do
author = create :user
project = create :empty_project
issue.notes.awards.create!(note: "thumbsup", author: author, project: project)
issue.notes.awards.create!(note: "thumbsdown", author: author, project: project)
end
it "returns correct values" do
expect(issue.upvotes).to eq(1)
expect(issue.downvotes).to eq(1)
end
end
end
......@@ -3,6 +3,10 @@ require 'spec_helper'
describe Mentionable do
include Mentionable
def author
nil
end
describe :references do
let(:project) { create(:project) }
......
......@@ -137,9 +137,14 @@ describe Note, models: true do
create :note, note: "smile", is_award: true
end
it "returns grouped array of notes" do
expect(Note.grouped_awards.first.first).to eq("smile")
expect(Note.grouped_awards.first.last).to match_array(Note.all)
it "returns grouped hash of notes" do
expect(Note.grouped_awards.keys.size).to eq(3)
expect(Note.grouped_awards["smile"]).to match_array(Note.all)
end
it "returns thumbsup and thumbsdown always" do
expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none)
expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none)
end
end
......
......@@ -27,6 +27,13 @@ describe API::API, api: true do
user['username'] == username
end['username']).to eq(username)
end
it "should return one user" do
get api("/users?username=#{omniauth_user.username}", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['username']).to eq(omniauth_user.username)
end
end
context "when admin" do
......
......@@ -115,6 +115,7 @@ describe NotificationService, services: true do
before do
build_team(note.project)
note.project.team << [note.author, :master]
ActionMailer::Base.deliveries.clear
end
......@@ -126,6 +127,8 @@ describe NotificationService, services: true do
note.project.team.members.each do |member|
# User with disabled notification should not be notified
next if member.id == @u_disabled.id
# Author should not be notified
next if member.id == note.author.id
should_email(member)
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