Commit ba410802 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into ce-to-ee[ci skip]

parents 2116c02a 9f999549
......@@ -1069,6 +1069,13 @@ RSpec/NotToNot:
RSpec/RepeatedDescription:
Enabled: false
# Ensure RSpec hook blocks are always multi-line.
RSpec/SingleLineHook:
Enabled: true
Exclude:
- 'spec/factories/*'
- 'spec/requests/api/v3/*'
# Checks for stubbed test subjects.
RSpec/SubjectStub:
Enabled: false
......
......@@ -274,6 +274,17 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development
# Perf bar
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
gem 'peek-host', '~> 1.0.0'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-performance_bar', '~> 1.2.1'
gem 'peek-pg', '~> 1.3.0', group: :postgres
gem 'peek-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0'
gem 'peek-sidekiq', '~> 1.0.3'
# Metrics
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
......
......@@ -56,6 +56,7 @@ GEM
asciidoctor-plantuml (0.0.7)
asciidoctor (~> 1.5)
ast (2.3.0)
atomic (1.1.99)
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
attr_required (1.0.0)
......@@ -67,13 +68,13 @@ GEM
execjs
json
awesome_print (1.2.0)
aws-sdk (2.7.8)
aws-sdk-resources (= 2.7.8)
aws-sdk-core (2.7.8)
aws-sdk (2.9.32)
aws-sdk-resources (= 2.9.32)
aws-sdk-core (2.9.32)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.7.8)
aws-sdk-core (= 2.7.8)
aws-sdk-resources (2.9.32)
aws-sdk-core (= 2.9.32)
aws-sigv4 (1.0.0)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
......@@ -139,6 +140,8 @@ GEM
coffee-script-source (1.10.0)
colorize (0.7.7)
concurrent-ruby (1.0.5)
concurrent-ruby-ext (1.0.5)
concurrent-ruby (= 1.0.5)
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
......@@ -218,8 +221,8 @@ GEM
multipart-post (>= 1.2, < 3)
faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 1.0)
faraday_middleware-aws-signers-v4 (0.1.5)
aws-sdk (~> 2.1)
faraday_middleware-aws-signers-v4 (0.1.7)
aws-sdk-resources (~> 2)
faraday (~> 0.9)
faraday_middleware-multi_json (0.0.6)
faraday_middleware
......@@ -576,6 +579,36 @@ GEM
parser (2.4.0.0)
ast (~> 2.2)
path_expander (1.0.1)
peek (1.0.1)
concurrent-ruby (>= 0.9.0)
concurrent-ruby-ext (>= 0.9.0)
railties (>= 4.0.0)
peek-gc (0.0.2)
peek
peek-host (1.0.0)
peek
peek-mysql2 (1.1.0)
atomic (>= 1.0.0)
mysql2
peek
peek-performance_bar (1.2.1)
peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
concurrent-ruby-ext
peek
pg
peek-rblineprof (0.2.0)
peek
rblineprof
peek-redis (1.2.0)
atomic (>= 1.0.0)
peek
redis
peek-sidekiq (1.0.3)
atomic (>= 1.0.0)
peek
sidekiq
pg (0.18.4)
po_to_json (1.0.1)
json (>= 1.6.0)
......@@ -1035,6 +1068,15 @@ DEPENDENCIES
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
paranoia (~> 2.2)
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-host (~> 1.0.0)
peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.2.1)
peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0)
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.0)
......
import 'vendor/peek';
import 'vendor/peek.performance_bar';
$(document).on('click', '#peek-show-queries', (e) => {
e.preventDefault();
$('.peek-rblineprof-modal').hide();
const $modal = $('#modal-peek-pg-queries');
if ($modal.length) {
$modal.modal('toggle');
}
});
$(document).on('click', '.js-lineprof-file', (e) => {
e.preventDefault();
$(e.target).parents('.peek-rblineprof-file').find('.data').toggle();
});
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
/* global findFileURL */
import Cookies from 'js-cookie';
import findAndFollowLink from './shortcuts_dashboard_navigation';
(function() {
......@@ -14,6 +16,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('f', (e => this.focusFilter(e)));
Mousetrap.bind('p b', this.onTogglePerfBar);
const $globalDropdownMenu = $('.global-dropdown-menu');
const $globalDropdownToggle = $('.global-dropdown-toggle');
......@@ -53,6 +56,17 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
return Shortcuts.toggleHelp(this.enabledHelp);
};
Shortcuts.prototype.onTogglePerfBar = function(e) {
e.preventDefault();
const performanceBarCookieName = 'perf_bar_enabled';
if (Cookies.get(performanceBarCookieName) === 'true') {
Cookies.remove(performanceBarCookieName, { path: '/' });
} else {
Cookies.set(performanceBarCookieName, true, { path: '/' });
}
gl.utils.refreshCurrentPage();
};
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode
const $target = $(e.target);
......
......@@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
include SentryHelper
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
include Peek::Rblineprof::CustomControllerHelpers
before_action :authenticate_user_from_private_token!
before_action :authenticate_user_from_rss_token!
......@@ -18,7 +19,7 @@ class ApplicationController < ActionController::Base
before_action :ldap_security_check
before_action :sentry_context
before_action :default_headers
before_action :add_gon_variables
before_action :add_gon_variables, unless: -> { request.path.start_with?('/-/peek') }
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
......@@ -63,6 +64,21 @@ class ApplicationController < ActionController::Base
end
end
def peek_enabled?
return false unless Gitlab::PerformanceBar.enabled?
return false unless current_user
if RequestStore.active?
if RequestStore.store.key?(:peek_enabled)
RequestStore.store[:peek_enabled]
else
RequestStore.store[:peek_enabled] = cookies[:perf_bar_enabled].present?
end
else
cookies[:perf_bar_enabled].present?
end
end
protected
# This filter handles both private tokens and personal access tokens
......
module Projects
class IssueLinksController < Projects::ApplicationController
before_action :authorize_admin_issue_link!, only: [:create, :destroy]
def index
render json: issues
end
def create
create_params = params.slice(:issue_references)
result = IssueLinks::CreateService.new(issue, current_user, create_params).execute
render json: { message: result[:message], issues: issues }, status: result[:http_status]
end
def destroy
issue_link = IssueLink.find(params[:id])
return render_403 unless can?(current_user, :admin_issue_link, issue_link.target.project)
IssueLinks::DestroyService.new(issue_link, current_user).execute
render json: { issues: issues }
end
private
def issues
IssueLinks::ListService.new(issue, current_user).execute
end
def authorize_admin_issue_link!
render_403 unless can?(current_user, :admin_issue_link, @project)
end
def issue
@issue ||=
IssuesFinder.new(current_user, project_id: @project.id)
.execute
.find_by!(iid: params[:issue_id])
end
end
end
......@@ -27,6 +27,7 @@ module NavHelper
def nav_header_class
class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav
class_name << " with-peek" if peek_enabled?
class_name
end
......
......@@ -276,6 +276,16 @@ class ApplicationSetting < ActiveRecord::Base
end
end
def elasticsearch_indexing
License.feature_available?(:elastic_search) && super
end
alias_method :elasticsearch_indexing?, :elasticsearch_indexing
def elasticsearch_search
License.feature_available?(:elastic_search) && super
end
alias_method :elasticsearch_search?, :elasticsearch_search
def elasticsearch_url
read_attribute(:elasticsearch_url).split(',').map(&:strip)
end
......
......@@ -6,6 +6,8 @@ class GeoNodeStatus
def health
@health ||= HealthCheck::Utils.process_checks(['geo'])
rescue NotImplementedError => e
@health = e.to_s
end
def healthy?
......
class IssueLink < ActiveRecord::Base
belongs_to :source, class_name: 'Issue'
belongs_to :target, class_name: 'Issue'
validates :source, presence: true
validates :target, presence: true
validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
validate :check_self_relation
private
def check_self_relation
return unless source && target
if source == target
errors.add(:source, 'cannot be related to itself')
end
end
end
......@@ -7,12 +7,16 @@ class License < ActiveRecord::Base
AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze
SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze
OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze
ELASTIC_SEARCH_FEATURE = 'GitLab_ElasticSearch'.freeze
RELATED_ISSUES_FEATURE = 'RelatedIssues'.freeze
FEATURE_CODES = {
geo: GEO_FEATURE,
auditor_user: AUDITOR_USER_FEATURE,
service_desk: SERVICE_DESK_FEATURE,
object_storage: OBJECT_STORAGE_FEATURE,
elastic_search: ELASTIC_SEARCH_FEATURE,
related_issues: RELATED_ISSUES_FEATURE,
# Features that make sense to Namespace:
deploy_board: DEPLOY_BOARD_FEATURE,
file_lock: FILE_LOCK_FEATURE
......@@ -24,7 +28,8 @@ class License < ActiveRecord::Base
EARLY_ADOPTER_PLAN = 'early_adopter'.freeze
EES_FEATURES = [
# ..
{ ELASTIC_SEARCH_FEATURE => 1 },
{ RELATED_ISSUES_FEATURE => 1 }
].freeze
EEP_FEATURES = [
......@@ -91,6 +96,8 @@ class License < ActiveRecord::Base
end
end
delegate :feature_available?, to: :current, allow_nil: true
def reset_current
RequestStore.delete(:current_license)
end
......@@ -183,7 +190,7 @@ class License < ActiveRecord::Base
end
def plan
restricted_attr(:plan)
restricted_attr(:plan, STARTER_PLAN)
end
def current_active_users_count
......
......@@ -344,10 +344,6 @@ class Project < ActiveRecord::Base
transition [:scheduled, :started] => :failed
end
event :import_retry do
transition failed: :started
end
state :scheduled
state :started
state :finished
......@@ -408,8 +404,6 @@ class Project < ActiveRecord::Base
after_transition [:finished, :failed] => [:scheduled, :started] do |project, _|
Gitlab::Mirror.increment_capacity(project.id) if project.mirror?
end
after_transition started: :finished, do: :reset_cache_and_import_attrs
end
class << self
......
......@@ -3,7 +3,7 @@ class SystemNoteMetadata < ActiveRecord::Base
commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved opened closed merged
outdated
approved unapproved
approved unapproved relate unrelate
].freeze
validates :note, presence: true
......
......@@ -22,6 +22,11 @@ module EE
cannot! :create_note
cannot! :read_project
end
unless project.feature_available?(:related_issues)
cannot! :read_issue_link
cannot! :admin_issue_link
end
end
end
end
......@@ -55,6 +55,9 @@ class ProjectPolicy < BasePolicy
can! :read_pipeline_schedule
can! :read_build
end
# EE-only
can! :read_issue_link
end
def reporter_access!
......@@ -79,6 +82,9 @@ class ProjectPolicy < BasePolicy
if project.feature_available?(:deploy_board) || Rails.env.development?
can! :read_deploy_board
end
# EE-only
can! :admin_issue_link
end
# Permissions given when an user is team member of a project
......@@ -321,5 +327,8 @@ class ProjectPolicy < BasePolicy
# NOTE: may be overridden by IssuePolicy
can! :read_issue
# EE-only
can! :read_issue_link
end
end
module IssueLinks
class CreateService < BaseService
def initialize(issue, user, params)
@issue, @current_user, @params = issue, user, params.dup
end
def execute
if referenced_issues.blank?
return error('No Issue found for given reference', 401)
end
create_issue_links
success
end
private
def create_issue_links
referenced_issues.each do |referenced_issue|
create_notes(referenced_issue) if relate_issues(referenced_issue)
end
end
def relate_issues(referenced_issue)
IssueLink.create(source: @issue, target: referenced_issue)
end
def create_notes(referenced_issue)
SystemNoteService.relate_issue(@issue, referenced_issue, current_user)
SystemNoteService.relate_issue(referenced_issue, @issue, current_user)
end
def referenced_issues
@referenced_issues ||= begin
issue_references = params[:issue_references]
text = issue_references.join(' ')
extractor = Gitlab::ReferenceExtractor.new(@issue.project, @current_user)
extractor.analyze(text)
extractor.issues.select do |issue|
can?(current_user, :admin_issue_link, issue)
end
end
end
end
end
module IssueLinks
class DestroyService < BaseService
def initialize(issue_link, user)
@issue_link = issue_link
@current_user = user
@issue = issue_link.source
@referenced_issue = issue_link.target
end
def execute
remove_relation
create_notes
success(message: 'Relation was removed')
end
private
def remove_relation
@issue_link.destroy!
end
def create_notes
SystemNoteService.unrelate_issue(@issue, @referenced_issue, current_user)
SystemNoteService.unrelate_issue(@referenced_issue, @issue, current_user)
end
end
end
module IssueLinks
class ListService
include Gitlab::Routing
def initialize(issue, user)
@issue, @current_user, @project = issue, user, issue.project
end
def execute
issues.map do |referenced_issue|
{
id: referenced_issue.id,
title: referenced_issue.title,
state: referenced_issue.state,
reference: referenced_issue.to_reference(@project),
path: namespace_project_issue_path(referenced_issue.project.namespace, referenced_issue.project, referenced_issue.iid),
destroy_relation_path: destroy_relation_path(referenced_issue)
}
end
end
private
def issues
related_issues = Issue
.select(['issues.*', 'issue_links.id AS issue_links_id'])
.joins("INNER JOIN issue_links ON
(issue_links.source_id = issues.id AND issue_links.target_id = #{@issue.id})
OR
(issue_links.target_id = issues.id AND issue_links.source_id = #{@issue.id})")
.preload(project: :namespace)
.reorder('issue_links_id')
Ability.issues_readable_by_user(related_issues, @current_user)
end
def destroy_relation_path(issue)
# Make sure the user can admin both the current issue AND the
# referenced issue projects in order to return the removal link.
if can_destroy_issue_link_on_current_project? && can_destroy_issue_link?(issue.project)
namespace_project_issue_link_path(@project.namespace,
@issue.project,
@issue.iid,
issue.issue_links_id)
end
end
def can_destroy_issue_link_on_current_project?
return @can_destroy_on_current_project if defined?(@can_destroy_on_current_project)
@can_destroy_on_current_project = can_destroy_issue_link?(@project)
end
def can_destroy_issue_link?(project)
Ability.allowed?(@current_user, :admin_issue_link, project)
end
end
end
......@@ -552,6 +552,38 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
end
#
# noteable - Noteable object
# noteable_ref - Referenced noteable object
# user - User performing reference
#
# Example Note text:
#
# "marked this issue as related to gitlab-ce#9001"
#
# Returns the created Note object
def relate_issue(noteable, noteable_ref, user)
body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'relate'))
end
#
# noteable - Noteable object
# noteable_ref - Referenced noteable object
# user - User performing reference
#
# Example Note text:
#
# "removed the relation with gitlab-ce#9001"
#
# Returns the created Note object
def unrelate_issue(noteable, noteable_ref, user)
body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'unrelate'))
end
# Called when the merge request is approved by user
#
# noteable - Noteable object
......
......@@ -120,8 +120,13 @@ class ObjectStoreUploader < CarrierWave::Uploader::Base
raise 'Object Storage feature is missing' unless subject.project.feature_available?(:object_storage)
end
<<<<<<< HEAD
def exists?
file.try(:exists?)
=======
def file_storage?
storage.is_a?(CarrierWave::Storage::File)
>>>>>>> 9f999549099fb5b254a3892d3b88284c39a4e12d
end
private
......
%fieldset
- if License.feature_available?(:elastic_search)
%fieldset
%legend Elasticsearch
.form-group
.col-sm-offset-2.col-sm-10
......@@ -31,7 +32,7 @@
.help-block
The url to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://localhost:9200, http://localhost:9201").
%fieldset
%fieldset
%legend Elasticsearch AWS IAM credentials
.form-group
.col-sm-offset-2.col-sm-10
......
......@@ -27,6 +27,10 @@
%td.shortcut
.key f
%td Focus Filter
%tr
%td.shortcut
.key p b
%td Show/hide the Performance Bar
%tr
%td.shortcut
.key ?
......
......@@ -28,6 +28,7 @@
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
= stylesheet_link_tag "test", media: "all" if Rails.env.test?
= stylesheet_link_tag 'peek' if peek_enabled?
= Gon::Base.render_data
......@@ -37,6 +38,7 @@
= webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test?
= webpack_bundle_tag 'peek' if peek_enabled?
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
......
......@@ -3,6 +3,7 @@
= render "layouts/head"
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar'
= render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar, nav: nav
......
- local_assigns.fetch(:view)
= render 'peek/views/sql', view: view
mysql
- local_assigns.fetch(:view)
= render 'peek/views/sql', view: view
pg
%strong
%a#peek-show-queries{ href: '#' }
%span{ data: { defer_to: "#{view.defer_key}-duration" } }...
\/
%span{ data: { defer_to: "#{view.defer_key}-calls" } }...
#modal-peek-pg-queries.modal{ tabindex: -1 }
.modal-dialog
#modal-peek-pg-queries-content.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h4
SQL queries
.modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }...
......@@ -11,6 +11,7 @@ module GeoDynamicBackoff
end
end
class_methods do
private
def linear_backoff_strategy(count)
......@@ -22,4 +23,5 @@ module GeoDynamicBackoff
count = count - 30 # we must start counting after 30
(count**4) + 15 + (rand(30) * (count + 1))
end
end
end
---
title: Allows manually adding bi-directional relationships between issues in the issue page (EES feature)
merge_request:
author:
---
title: Add explicit licensing for Elasticsearch
merge_request: 2108
author:
---
title: 'Geo: fixed Dynamic Backoff strategy that was not being used by workers'
merge_request: 2128
author:
---
title: Adding support for AWS ec2 instance profile credentials with elasticsearch
merge_request:
author: Matt Gresko
---
title: Add an optional performance bar to view performance metrics for the current page
merge_request: 11439
author:
......@@ -111,6 +111,7 @@ module Gitlab
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css"
config.assets.precompile << "peek.css"
config.assets.precompile << "lib/ace.js"
config.assets.precompile << "vendor/assets/fonts/*"
config.assets.precompile << "test.css"
......
......@@ -8,7 +8,7 @@ end
begin
# Avoid using the database if this is run in a Rake task
if Gitlab::Geo.primary_role_enabled?
Gitlab::Geo.current_node.update_clone_url!
Gitlab::Geo.current_node&.update_clone_url!
end
rescue => e
warn "WARNING: Unable to check/update clone_url_prefix for Geo: #{e}"
......
Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Redis.params) }
Peek.into Peek::Views::Host
Peek.into Peek::Views::PerformanceBar
if Gitlab::Database.mysql?
require 'peek-mysql2'
PEEK_DB_CLIENT = ::Mysql2::Client
PEEK_DB_VIEW = Peek::Views::Mysql2
else
require 'peek-pg'
PEEK_DB_CLIENT = ::PG::Connection
PEEK_DB_VIEW = Peek::Views::PG
end
Peek.into PEEK_DB_VIEW
Peek.into Peek::Views::Redis
Peek.into Peek::Views::Sidekiq
Peek.into Peek::Views::Rblineprof
Peek.into Peek::Views::GC
# rubocop:disable Style/ClassAndModuleCamelCase
class PEEK_DB_CLIENT
class << self
attr_accessor :query_details
end
self.query_details = Concurrent::Array.new
end
PEEK_DB_VIEW.prepend ::Gitlab::PerformanceBar::PeekQueryTracker
class Peek::Views::PerformanceBar::ProcessUtilization
prepend ::Gitlab::PerformanceBar::PeekPerformanceBarWithRackBody
end
......@@ -3,6 +3,11 @@
en:
hello: "Hello world"
activerecord:
attributes:
issue_link:
source: Source issue
target: Target issue
errors:
messages:
label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
......
......@@ -51,6 +51,7 @@ Rails.application.routes.draw do
get 'liveness' => 'health#liveness'
get 'readiness' => 'health#readiness'
resources :metrics, only: [:index]
mount Peek::Railtie => '/peek'
end
# Koding route
......
......@@ -311,6 +311,8 @@ constraints(ProjectUrlConstrainer.new) do
post :bulk_update
post :export_csv
end
resources :issue_links, only: [:index, :create, :destroy], as: 'links', path: 'links'
end
resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
......
......@@ -71,6 +71,7 @@ var config = {
raven: './raven/index.js',
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
peek: './peek.js',
},
output: {
......
class CreateIssueLinksTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :issue_links do |t|
t.integer :source_id, null: false, index: true
t.integer :target_id, null: false, index: true
t.timestamps null: true
end
add_index :issue_links, [:source_id, :target_id], unique: true
add_concurrent_foreign_key :issue_links, :issues, column: :source_id
add_concurrent_foreign_key :issue_links, :issues, column: :target_id
end
def down
drop_table :issue_links
end
end
......@@ -654,6 +654,17 @@ ActiveRecord::Schema.define(version: 20170606202615) do
add_index "issue_assignees", ["issue_id", "user_id"], name: "index_issue_assignees_on_issue_id_and_user_id", unique: true, using: :btree
add_index "issue_assignees", ["user_id"], name: "index_issue_assignees_on_user_id", using: :btree
create_table "issue_links", force: :cascade do |t|
t.integer "source_id", null: false
t.integer "target_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "issue_links", ["source_id", "target_id"], name: "index_issue_links_on_source_id_and_target_id", unique: true, using: :btree
add_index "issue_links", ["source_id"], name: "index_issue_links_on_source_id", using: :btree
add_index "issue_links", ["target_id"], name: "index_issue_links_on_target_id", using: :btree
create_table "issue_metrics", force: :cascade do |t|
t.integer "issue_id", null: false
t.datetime "first_mentioned_in_commit_at"
......@@ -1779,6 +1790,8 @@ ActiveRecord::Schema.define(version: 20170606202615) do
add_foreign_key "geo_repository_updated_events", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_links", "issues", column: "source_id", name: "fk_c900194ff2", on_delete: :cascade
add_foreign_key "issue_links", "issues", column: "target_id", name: "fk_e71bb44f1f", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
......
......@@ -56,6 +56,7 @@ following locations:
- [V3 to V4](v3_to_v4.md)
- [Version](version.md)
<<<<<<< HEAD
## Road to GraphQL
Going forward, we will start on moving to
......@@ -71,17 +72,37 @@ compatability layer on top of GraphQL.
### Internal CI API
=======
>>>>>>> 9f999549099fb5b254a3892d3b88284c39a4e12d
The following documentation is for the [internal CI API](ci/README.md):
- [Builds](ci/builds.md)
- [Runners](ci/runners.md)
## Road to GraphQL
Going forward, we will start on moving to
[GraphQL](http://graphql.org/learn/best-practices/) and deprecate the use of
controller-specific endpoints. GraphQL has a number of benefits:
1. We avoid having to maintain two different APIs.
2. Callers of the API can request only what they need.
3. It is versioned by default.
It will co-exist with the current v4 REST API. If we have a v5 API, this should
be a compatibility layer on top of GraphQL.
## Authentication
Most API requests require authentication via a session cookie or token. For those cases where it is not required, this will be mentioned in the documentation
Most API requests require authentication via a session cookie or token. For
those cases where it is not required, this will be mentioned in the documentation
for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md).
There are three types of tokens available: private tokens, OAuth 2 tokens, and personal
access tokens.
There are three types of access tokens available:
1. [OAuth2 tokens](#oauth2-tokens)
1. [Private tokens](#private-tokens)
1. [Personal access tokens](#personal-access-tokens)
If authentication information is invalid or omitted, an error message will be
returned with status code `401`:
......@@ -92,20 +113,13 @@ returned with status code `401`:
}
```
### Session Cookie
### Session cookie
When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
set. The API will use this cookie for authentication if it is present, but using
the API to generate a new session cookie is currently not supported.
### Private Tokens
You need to pass a `private_token` parameter via query string or header. If passed as a
header, the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of
an underscore). You can find or reset your private token in your account page
(`/profile/account`).
### OAuth 2 Tokens
### OAuth2 tokens
You can use an OAuth 2 token to authenticate with the API by passing it either in the
`access_token` parameter or in the `Authorization` header.
......@@ -118,30 +132,31 @@ curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api
Read more about [GitLab as an OAuth2 client](oauth2.md).
### Personal Access Tokens
### Private tokens
> [Introduced][ce-3749] in GitLab 8.8.
Private tokens provide full access to the GitLab API. Anyone with access to
them can interact with GitLab as if they were you. You can find or reset your
private token in your account page (`/profile/account`).
You can create as many personal access tokens as you like from your GitLab
profile (`/profile/personal_access_tokens`); perhaps one for each application
that needs access to the GitLab API.
For examples of usage, [read the basic usage section](#basic-usage).
Once you have your token, pass it to the API using either the `private_token`
parameter or the `PRIVATE-TOKEN` header.
### Personal access tokens
> [Introduced][ce-5951] in GitLab 8.15.
Instead of using your private token which grants full access to your account,
personal access tokens could be a better fit because of their granular
permissions.
Personal Access Tokens can be created with one or more scopes that allow various actions
that a given token can perform. Although there are only two scopes available at the
moment – `read_user` and `api` – the groundwork has been laid to add more scopes easily.
Once you have your token, pass it to the API using either the `private_token`
parameter or the `PRIVATE-TOKEN` header. For examples of usage,
[read the basic usage section](#basic-usage).
At any time you can revoke any personal access token by just clicking **Revoke**.
[Read more about personal access tokens.][pat]
### Impersonation tokens
> [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions.
Impersonation tokens are a type of [Personal Access Token](#personal-access-tokens)
Impersonation tokens are a type of [personal access token][pat]
that can only be created by an admin for a specific user.
They are a better alternative to using the user's password/private token
......@@ -150,9 +165,11 @@ or private token, since the password/token can change over time. Impersonation
tokens are a great fit if you want to build applications or tools which
authenticate with the API as a specific user.
For more information about the usage please refer to the
For more information, refer to the
[users API](users.md#retrieve-user-impersonation-tokens) docs.
For examples of usage, [read the basic usage section](#basic-usage).
### Sudo
> Needs admin permissions.
......@@ -205,11 +222,16 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects"
```
## Basic Usage
## Basic usage
API requests should be prefixed with `api` and the API version. The API version
is defined in [`lib/api.rb`][lib-api-url].
For endpoints that require [authentication](#authentication), you need to pass
a `private_token` parameter via query string or header. If passed as a header,
the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of
an underscore).
Example of a valid API request:
```
......@@ -222,6 +244,12 @@ Example of a valid API request using cURL and authentication via header:
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
```
Example of a valid API request using cURL and authentication via a query string:
```shell
curl "https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK"
```
The API uses JSON to serialize data. You don't need to specify `.json` at the
end of an API URL.
......@@ -437,3 +465,4 @@ programming languages. Visit the [GitLab website] for a complete list.
[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951
[ce-9099]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9099
[pat]: ../user/profile/personal_access_tokens.md
......@@ -134,4 +134,4 @@ access_token = client.password.get_token('user@example.com', 'secret')
puts access_token.token
```
[personal access tokens]: ./README.md#personal-access-tokens
[personal access tokens]: ../user/profile/personal_access_tokens.md
# Session API
## Deprecation Notice
1. Starting in GitLab 8.11, this feature has been *disabled* for users with two-factor authentication turned on.
2. These users can access the API using [personal access tokens] instead.
---
>**Deprecation notice:**
Starting in GitLab 8.11, this feature has been **disabled** for users with
[two-factor authentication][2fa] turned on. These users can access the API
using [personal access tokens] instead.
You can login with both GitLab and LDAP credentials in order to obtain the
private token.
......@@ -52,4 +50,5 @@ Example response:
}
```
[personal access tokens]: ./README.md#personal-access-tokens
[2fa]: ../user/profile/account/two_factor_authentication.md
[personal access tokens]: ../user/profile/personal_access_tokens.md
......@@ -809,7 +809,7 @@ Example response:
It creates a new impersonation token. Note that only administrators can do this.
You are only able to create impersonation tokens to impersonate the user and perform
both API calls and Git reads and writes. The user will not see these tokens in his profile
both API calls and Git reads and writes. The user will not see these tokens in their profile
settings page.
```
......
......@@ -282,9 +282,9 @@ which can be avoided if a different driver is used, for example `overlay`.
> **Notes:**
- This feature requires GitLab 8.8 and GitLab Runner 1.2.
- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need
to pass a personal access token instead of your password in order to login to
GitLab's Container Registry.
- Starting from GitLab 8.12, if you have [2FA] enabled in your account, you need
to pass a [personal access token][pat] instead of your password in order to
login to GitLab's Container Registry.
Once you've built a Docker image, you can push it up to the built-in
[GitLab Container Registry](../../user/project/container_registry.md). For example,
......@@ -409,3 +409,5 @@ Some things you should be aware of when using the Container Registry:
[docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/
[docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
[2fa]: ../../user/profile/account/two_factor_authentication.md
[pat]: ../../user/profile/personal_access_tokens.md
......@@ -27,7 +27,7 @@ download and analyze the report artifact in JSON format.
For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically
extracted and shown right in the merge request widget. [Learn more on code quality
diffs in merge requests](http://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.md).
diffs in merge requests](../../user/project/merge_requests/code_quality_diff.md).
[cli]: https://github.com/codeclimate/codeclimate
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
......
......@@ -31,6 +31,38 @@ Keep in mind that:
clone/pull from repositories (HTTP(S)/SSH).
- Primary talks to secondaries to notify for changes (API).
## Architecture
The following diagram illustrates the underlying architecture of GitLab Geo:
![GitLab Geo architecture](img/geo-architecture.png)
[Source diagram](https://docs.google.com/drawings/d/1VQIcj6jyE3idWKyt9MRUAaE3XXrkwx8g-Ne4pmURmwI/edit)
In this diagram, there is one Geo primary node and one secondary. The
secondary clones repositories via git over SSH. Attachments, LFS objects, and
other files are downloaded via HTTPS using a GitLab API to authenticate.
Writes to the database and Git repositories can only be performed on the Geo
primary node. The secondary node receives database updates via PostgreSQL
streaming replication.
Note that the secondary needs two different PostgreSQL databases: a read-only
instance that streams data from the main GitLab database and another used
internally by the secondary node to record what data has been replicated.
### LDAP
We recommend that if you use LDAP on your primary that you also set up a
secondary LDAP server for the secondary Geo node. Otherwise, users will not be
able to perform Git operations over HTTP(s) on the **secondary** Geo node
using HTTP Basic Authentication. However, Git via SSH and personal access
tokens will still work.
Check with your LDAP provider for instructions on on how to set up
replication. For example, OpenLDAP provides [these
instructions](https://www.openldap.org/doc/admin24/replication.html).
## Setup instructions
In order to set up one or more GitLab Geo instances, follow the steps below in
......@@ -47,6 +79,7 @@ If you installed GitLab using the Omnibus packages (highly recommended):
1. [Upload the GitLab License](../user/admin_area/license.md) to the **primary** Geo Node to unlock GitLab Geo.
1. [Setup the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology).
1. [Configure GitLab](configuration.md) to set the primary and secondary nodes.
1. Optional: [Configure a secondary LDAP server](../administration/auth/ldap.md) for the secondary. See [notes on LDAP](#ldap).
1. [Follow the after setup steps](after_setup.md).
[install-ee]: https://about.gitlab.com/downloads-ee/ "GitLab Enterprise Edition Omnibus packages downloads page"
......
......@@ -58,7 +58,7 @@ The following Elasticsearch settings are available:
| `Use experimental repository indexer` | Perform repository indexing using [GitLab Elasticsearch Indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). |
| `Search with Elasticsearch enabled` | Enables/disables using Elasticsearch in search. |
| `URL` | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://host1, https://host2:9200"). |
| `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization][aws-iam]. The access key must be allowed to perform `es:*` actions. |
| `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization][aws-iam] or [AWS EC2 Instance Profile Credentials][aws-instance-profile]. The policies must be configured to allow `es:*` actions. |
| `AWS Region` | The AWS region your Elasticsearch service is located in. |
| `AWS Access Key` | The AWS access key. |
| `AWS Secret Access Key` | The AWS secret access key. |
......@@ -325,6 +325,7 @@ Make sure you indexed all the database data as stated above (`sudo gitlab-rake g
[ee-1305]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1305
[aws-elasticsearch]: http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-gsg.html
[aws-iam]: http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html
[aws-instance-profile]: http://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli
[ee-109]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/109 "Elasticsearch Merge Request"
[elasticsearch]: https://www.elastic.co/products/elasticsearch "Elasticsearch website"
[install]: https://www.elastic.co/guide/en/elasticsearch/reference/current/_installation.html "Elasticsearch installation documentation"
......
......@@ -125,23 +125,14 @@ applications and U2F devices.
## Personal access tokens
When 2FA is enabled, you can no longer use your normal account password to
authenticate with Git over HTTPS on the command line, you must use a personal
access token instead.
1. Log in to your GitLab account.
1. Go to your **Profile Settings**.
1. Go to **Access Tokens**.
1. Choose a name and expiry date for the token.
1. Click on **Create Personal Access Token**.
1. Save the personal access token somewhere safe.
When using Git over HTTPS on the command line, enter the personal access token
into the password field.
authenticate with Git over HTTPS on the command line or when using
[GitLab's API][api], you must use a [personal access token][pat] instead.
## Recovery options
To disable two-factor authentication on your account (for example, if you
have lost your code generation device) you can:
* [Use a saved recovery code](#use-a-saved-recovery-code)
* [Generate new recovery codes using SSH](#generate-new-recovery-codes-using-ssh)
* [Ask a GitLab administrator to disable two-factor authentication on your account](#ask-a-gitlab-administrator-to-disable-two-factor-authentication-on-your-account)
......@@ -154,8 +145,9 @@ codes. If you saved these codes, you can use one of them to sign in.
To use a recovery code, enter your username/email and password on the GitLab
sign-in page. When prompted for a two-factor code, enter the recovery code.
> **Note:** Once you use a recovery code, you cannot re-use it. You can still
use the other recovery codes you saved.
>**Note:**
Once you use a recovery code, you cannot re-use it. You can still use the other
recovery codes you saved.
### Generate new recovery codes using SSH
......@@ -190,11 +182,14 @@ a new set of recovery codes with SSH.
two-factor code. Then, visit your Profile Settings and add a new device
so you do not lose access to your account again.
```
3. Go to the GitLab sign-in page and enter your username/email and password. When prompted for a two-factor code, enter one of the recovery codes obtained
from the command-line output.
> **Note:** After signing in, visit your **Profile Settings -> Account** immediately to set up two-factor authentication with a new
device.
3. Go to the GitLab sign-in page and enter your username/email and password.
When prompted for a two-factor code, enter one of the recovery codes obtained
from the command-line output.
>**Note:**
After signing in, visit your **Profile settings > Account** immediately to set
up two-factor authentication with a new device.
### Ask a GitLab administrator to disable two-factor authentication on your account
......@@ -206,19 +201,13 @@ Sign in and re-enable two-factor authentication as soon as possible.
## Note to GitLab administrators
- You need to take special care to that 2FA keeps working after
[restoring a GitLab backup](../../../raketasks/backup_restore.md).
[restoring a GitLab backup](../../../raketasks/backup_restore.md).
- To ensure 2FA authorizes correctly with TOTP server, you may want to ensure
your GitLab server's time is synchronized via a service like NTP. Otherwise,
you may have cases where authorization always fails because of time differences.
[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
[FreeOTP]: https://freeotp.github.io/
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
your GitLab server's time is synchronized via a service like NTP. Otherwise,
you may have cases where authorization always fails because of time differences.
- The GitLab U2F implementation does _not_ work when the GitLab instance is accessed from
multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at
the time of registration, and cannot be used for other hostnames/FQDNs.
multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at
the time of registration, and cannot be used for other hostnames/FQDNs.
For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`:
......@@ -226,3 +215,9 @@ the time of registration, and cannot be used for other hostnames/FQDNs.
- The user logs out and attempts to log in via `first.host.xyz` - U2F authentication suceeds.
- The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because
the U2F key has only been registered on `first.host.xyz`.
[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
[FreeOTP]: https://freeotp.github.io/
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
[api]: ../../../api/README.md
[pat]: ../personal_access_tokens.md
# Personal access tokens
> [Introduced][ce-3749] in GitLab 8.8.
Personal access tokens are useful if you need access to the [GitLab API][api].
Instead of using your private token which grants full access to your account,
personal access tokens could be a better fit because of their
[granular permissions](#limiting-scopes-of-a-personal-access-token).
You can also use them to authenticate against Git over HTTP. They are the only
accepted method of authentication when you have
[Two-Factor Authentication (2FA)][2fa] enabled.
Once you have your token, [pass it to the API][usage] using either the
`private_token` parameter or the `PRIVATE-TOKEN` header.
## Creating a personal access token
You can create as many personal access tokens as you like from your GitLab
profile.
1. Log in to your GitLab account.
1. Go to your **Profile settings**.
1. Go to **Access tokens**.
1. Choose a name and optionally an expiry date for the token.
1. Choose the [desired scopes](#limiting-scopes-of-a-personal-access-token).
1. Click on **Create personal access token**.
1. Save the personal access token somewhere safe. Once you leave or refresh
the page, you won't be able to access it again.
![Personal access tokens page](img/personal_access_tokens.png)
## Revoking a personal access token
At any time, you can revoke any personal access token by just clicking the
respective **Revoke** button under the 'Active personal access tokens' area.
## Limiting scopes of a personal access token
Personal access tokens can be created with one or more scopes that allow various
actions that a given token can perform. The available scopes are depicted in
the following table.
| Scope | Description |
| ----- | ----------- |
|`read_user` | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed ([introduced][ce-5951] in GitLab 8.15). |
| `api` | Grants complete access to the API (read/write) ([introduced][ce-5951] in GitLab 8.15). Required for accessing Git repositories over HTTP when 2FA is enabled. |
| `read_registry` | Allows to read [container registry] images if a project is private and authorization is required ([introduced][ce-11845] in GitLab 9.3). |
[2fa]: ../account/two_factor_authentication.md
[api]: ../../api/README.md
[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951
[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845
[container registry]: ../project/container_registry.md
[users]: ../../api/users.md
[usage]: ../../api/README.md#basic-usage
......@@ -8,8 +8,8 @@
Registry across your GitLab instance, visit the
[administrator documentation](../../administration/container_registry.md).
- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need
to pass a personal access token instead of your password in order to login to
GitLab's Container Registry.
to pass a [personal access token][pat] instead of your password in order to
login to GitLab's Container Registry.
- Multiple level image names support was added in GitLab 9.1
With the Docker Container Registry integrated into GitLab, every project can
......@@ -114,12 +114,11 @@ and [Using the GitLab Container Registry documentation](../../ci/docker/using_do
## Using with private projects
If a project is private, credentials will need to be provided for authorization.
The preferred way to do this, is by using personal access tokens, which can be
created under `/profile/personal_access_tokens`. The minimal scope needed is:
`read_registry`.
> [Introduced][ce-11845] in GitLab 9.3.
This feature was introduced in GitLab 9.3.
If a project is private, credentials will need to be provided for authorization.
The preferred way to do this, is by using [personal access tokens][pat].
The minimal scope needed is `read_registry`.
## Troubleshooting the GitLab Container Registry
......@@ -264,4 +263,6 @@ The solution: check the [IAM permissions again](https://docs.docker.com/registry
Once the right permissions were set, the error will go away.
[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845
[docker-docs]: https://docs.docker.com/engine/userguide/intro/
[pat]: ../profile/personal_access_tokens.md
......@@ -212,9 +212,9 @@ Container Registries for private projects.
access token created explicitly for this purpose). This issue is resolved with
latest changes in GitLab Runner 1.8 which receives GitLab credentials with
build data.
- Starting with GitLab 8.12, if you have 2FA enabled in your account, you need
to pass a personal access token instead of your password in order to login to
GitLab's Container Registry.
- Starting from GitLab 8.12, if you have [2FA] enabled in your account, you need
to pass a [personal access token][pat] instead of your password in order to
login to GitLab's Container Registry.
Your jobs can access all container images that you would normally have access
to. The only implication is that you can push to the Container Registry of the
......@@ -239,3 +239,5 @@ test:
[update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update
[workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
[jobenv]: ../../ci/variables/README.md#predefined-variables-environment-variables
[2fa]: ../profile/account/two_factor_authentication.md
[pat]: ../profile/personal_access_tokens.md
......@@ -6,7 +6,10 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
| <kbd>n</kbd> | Main navigation |
| <kbd>s</kbd> | Focus search |
| <kbd>f</kbd> | Focus filter |
| <kbd>p b</kbd> | Show/hide the Performance Bar |
| <kbd>?</kbd> | Show/hide this dialog |
| <kbd></kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
| <kbd></kbd> | Edit last comment (when focused on an empty textarea) |
......
......@@ -11,8 +11,10 @@ Capybara.register_driver :poltergeist do |app|
js_errors: true,
timeout: timeout,
window_size: [1366, 768],
url_whitelist: %w[localhost 127.0.0.1],
url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg],
phantomjs_options: [
'--load-images=no'
'--load-images=yes'
]
)
end
......
......@@ -13,7 +13,7 @@ module Gitlab
base_config = { urls: config[:url] }
if config[:aws]
creds = Aws::Credentials.new(config[:aws_access_key], config[:aws_secret_access_key])
creds = resolve_aws_credentials(config)
region = config[:aws_region]
::Elasticsearch::Client.new(base_config) do |fmid|
......@@ -23,6 +23,19 @@ module Gitlab
::Elasticsearch::Client.new(base_config)
end
end
def self.resolve_aws_credentials(config)
# Resolve credentials in order
# 1. Static config
# 2. ec2 instance profile
credentials = [
Aws::Credentials.new(config[:aws_access_key], config[:aws_secret_access_key]),
Aws::InstanceProfileCredentials.new
]
credentials.find do |creds|
creds&.set?
end
end
end
end
end
......@@ -2,6 +2,8 @@ module Gitlab
module Geo
class HealthCheck
def self.perform_checks
raise NotImplementedError.new('Geo is only compatible with PostgreSQL') unless Gitlab::Database.postgresql?
return '' unless Gitlab::Geo.secondary?
return 'The Geo secondary role is disabled.' unless Gitlab::Geo.secondary_role_enabled?
return 'The Geo database configuration file is missing.' unless self.geo_database_configured?
......@@ -11,7 +13,8 @@ module Gitlab
migration_version = self.get_migration_version.to_i
if database_version != migration_version
"Current Geo database version (#{database_version}) does not match latest migration (#{migration_version})."
"Current Geo database version (#{database_version}) does not match latest migration (#{migration_version}).\n"\
"You may have to run `gitlab-rake geo:db:migrate` as root on the secondary."
else
''
end
......
module Gitlab
module PerformanceBar
def self.enabled?
Feature.enabled?('gitlab_performance_bar')
end
end
end
# This solves a bug with a X-Senfile header that wouldn't be set properly, see
# https://github.com/peek/peek-performance_bar/pull/27
module Gitlab
module PerformanceBar
module PeekPerformanceBarWithRackBody
def call(env)
@env = env
reset_stats
@total_requests += 1
first_request if @total_requests == 1
env['process.request_start'] = @start.to_f
env['process.total_requests'] = total_requests
status, headers, body = @app.call(env)
body = Rack::BodyProxy.new(body) { record_request }
[status, headers, body]
end
end
end
end
# Inspired by https://github.com/peek/peek-pg/blob/master/lib/peek/views/pg.rb
module Gitlab
module PerformanceBar
module PeekQueryTracker
def sorted_queries
PEEK_DB_CLIENT.query_details.
sort { |a, b| b[:duration] <=> a[:duration] }
end
def results
super.merge(queries: sorted_queries)
end
private
def setup_subscribers
super
# Reset each counter when a new request starts
before_request do
PEEK_DB_CLIENT.query_details = []
end
subscribe('sql.active_record') do |_, start, finish, _, data|
if RequestStore.active? && RequestStore.store[:peek_enabled]
track_query(data[:sql].strip, data[:binds], start, finish)
end
end
end
def track_query(raw_query, bindings, start, finish)
query = Gitlab::Sherlock::Query.new(raw_query, start, finish)
query_info = { duration: '%.3f' % query.duration, sql: query.formatted_query }
PEEK_DB_CLIENT.query_details << query_info
end
end
end
end
module Peek
module Rblineprof
module CustomControllerHelpers
extend ActiveSupport::Concern
# This will become useless once https://github.com/peek/peek-rblineprof/pull/5
# is merged
def pygmentize(file_name, code, lexer = nil)
if lexer.present?
Gitlab::Highlight.highlight(file_name, code)
else
"<pre>#{Rack::Utils.escape_html(code)}</pre>"
end
end
# rubocop:disable all
def inject_rblineprof
ret = nil
profile = lineprof(rblineprof_profiler_regex) do
ret = yield
end
if response.content_type =~ %r|text/html|
sort = params[:lineprofiler_sort]
mode = params[:lineprofiler_mode] || 'cpu'
min = (params[:lineprofiler_min] || 5).to_i * 1000
summary = params[:lineprofiler_summary]
# Sort each file by the longest calculated time
per_file = profile.map do |file, lines|
total, child, excl, total_cpu, child_cpu, excl_cpu = lines[0]
wall = summary == 'exclusive' ? excl : total
cpu = summary == 'exclusive' ? excl_cpu : total_cpu
idle = summary == 'exclusive' ? (excl - excl_cpu) : (total - total_cpu)
[
file, lines,
wall, cpu, idle,
sort == 'idle' ? idle : sort == 'cpu' ? cpu : wall
]
end.sort_by{ |a,b,c,d,e,f| -f }
output = ''
per_file.each do |file_name, lines, file_wall, file_cpu, file_idle, file_sort|
output << "<div class='peek-rblineprof-file'><div class='heading'>"
show_src = file_sort > min
tmpl = show_src ? "<a href='#' class='js-lineprof-file'>%s</a>" : "%s"
if mode == 'cpu'
output << sprintf("<span class='duration'>% 8.1fms + % 8.1fms</span> #{tmpl}", file_cpu / 1000.0, file_idle / 1000.0, file_name.sub(Rails.root.to_s + '/', ''))
else
output << sprintf("<span class='duration'>% 8.1fms</span> #{tmpl}", file_wall/1000.0, file_name.sub(Rails.root.to_s + '/', ''))
end
output << "</div>" # .heading
next unless show_src
output << "<div class='data'>"
code = []
times = []
File.readlines(file_name).each_with_index do |line, i|
code << line
wall, cpu, calls = lines[i + 1]
if calls && calls > 0
if mode == 'cpu'
idle = wall - cpu
times << sprintf("% 8.1fms + % 8.1fms (% 5d)", cpu / 1000.0, idle / 1000.0, calls)
else
times << sprintf("% 8.1fms (% 5d)", wall / 1000.0, calls)
end
else
times << ' '
end
end
output << "<pre class='duration'>#{times.join("\n")}</pre>"
# The following line was changed from
# https://github.com/peek/peek-rblineprof/blob/8d3b7a283a27de2f40abda45974516693d882258/lib/peek/rblineprof/controller_helpers.rb#L125
# This will become useless once https://github.com/peek/peek-rblineprof/pull/16
# is merged and is implemented.
output << "<pre class='code highlight white'>#{pygmentize(file_name, code.join, 'ruby')}</pre>"
output << "</div></div>" # .data then .peek-rblineprof-file
end
response.body += "<div class='peek-rblineprof-modal' id='line-profile'>#{output}</div>".html_safe
end
ret
end
end
end
end
module RuboCop
module Cop
module RSpec
# This cop checks for single-line hook blocks
#
# @example
#
# # bad
# before { do_something }
# after(:each) { undo_something }
#
# # good
# before do
# do_something
# end
#
# after(:each) do
# undo_something
# end
class SingleLineHook < RuboCop::Cop::RSpec::Cop
MESSAGE = "Don't use single-line hook blocks.".freeze
def_node_search :rspec_hook?, <<~PATTERN
(send nil {:after :around :before} ...)
PATTERN
def on_block(node)
return unless rspec_hook?(node)
return unless node.single_line?
add_offense(node, :expression, MESSAGE)
end
end
end
end
end
......@@ -12,3 +12,4 @@ require_relative 'cop/migration/remove_concurrent_index'
require_relative 'cop/migration/remove_index'
require_relative 'cop/migration/reversible_add_column_with_default'
require_relative 'cop/migration/update_column_in_batches'
require_relative 'cop/rspec/single_line_hook'
......@@ -3,7 +3,7 @@
require ::File.expand_path('../lib/gitlab/popen', __dir__)
tasks = [
%w[bundle exec bundle-audit check --update --ignore CVE-2016-4658],
%w[bundle exec bundle-audit check --update --ignore CVE-2016-4658 CVE-2017-5029],
%w[bundle exec rake config_lint],
%w[bundle exec rake flay],
%w[bundle exec rake haml_lint],
......
require 'spec_helper'
describe Admin::GeoNodesController do
describe Admin::GeoNodesController, :postgresql do
shared_examples 'unlicensed geo action' do
it 'redirects to the license page' do
expect(response).to redirect_to(admin_license_path)
......
......@@ -2,7 +2,10 @@ require 'spec_helper'
describe Admin::IdentitiesController do
let(:admin) { create(:admin) }
before { sign_in(admin) }
before do
sign_in(admin)
end
describe 'UPDATE identity' do
let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') }
......
......@@ -2,7 +2,10 @@ require 'spec_helper'
describe Admin::LicensesController do
let(:admin) { create(:admin) }
before { sign_in(admin) }
before do
sign_in(admin)
end
describe 'Upload license' do
it 'redirects back when no license is entered/uploaded' do
......
......@@ -3,7 +3,9 @@ require 'spec_helper'
describe Admin::ServicesController do
let(:admin) { create(:admin) }
before { sign_in(admin) }
before do
sign_in(admin)
end
describe 'GET #edit' do
let!(:project) { create(:empty_project) }
......
......@@ -216,6 +216,7 @@ describe AutocompleteController do
before do
sign_in(user)
end
<<<<<<< HEAD
it 'includes the author' do
get(:users, author_id: non_member.id)
......@@ -226,6 +227,18 @@ describe AutocompleteController do
it 'rejects non existent user ids' do
get(:users, author_id: 99999)
=======
it 'includes the author' do
get(:users, author_id: non_member.id)
expect(body.first["username"]).to eq non_member.username
end
it 'rejects non existent user ids' do
get(:users, author_id: 99999)
>>>>>>> 9f999549099fb5b254a3892d3b88284c39a4e12d
expect(body.collect { |u| u['id'] }).not_to include(99999)
end
end
......@@ -240,7 +253,9 @@ describe AutocompleteController do
end
context 'skip_users parameter included' do
before { sign_in(user) }
before do
sign_in(user)
end
it 'skips the user IDs passed' do
get(:users, skip_users: [user, user2].map(&:id))
......
......@@ -16,10 +16,14 @@ describe Groups::GroupMembersController do
describe 'POST create' do
let(:group_user) { create(:user) }
before { sign_in(user) }
before do
sign_in(user)
end
context 'when user does not have enough rights' do
before { group.add_developer(user) }
before do
group.add_developer(user)
end
it 'returns 403' do
post :create, group_id: group,
......@@ -32,7 +36,9 @@ describe Groups::GroupMembersController do
end
context 'when user has enough rights' do
before { group.add_owner(user) }
before do
group.add_owner(user)
end
it 'adds user to members' do
post :create, group_id: group,
......@@ -59,7 +65,9 @@ describe Groups::GroupMembersController do
describe 'DELETE destroy' do
let(:member) { create(:group_member, :developer, group: group) }
before { sign_in(user) }
before do
sign_in(user)
end
context 'when member is not found' do
it 'returns 403' do
......@@ -71,7 +79,9 @@ describe Groups::GroupMembersController do
context 'when member is found' do
context 'when user does not have enough rights' do
before { group.add_developer(user) }
before do
group.add_developer(user)
end
it 'returns 403' do
delete :destroy, group_id: group, id: member
......@@ -82,7 +92,9 @@ describe Groups::GroupMembersController do
end
context 'when user has enough rights' do
before { group.add_owner(user) }
before do
group.add_owner(user)
end
it '[HTML] removes user from members' do
delete :destroy, group_id: group, id: member
......@@ -103,7 +115,9 @@ describe Groups::GroupMembersController do
end
describe 'DELETE leave' do
before { sign_in(user) }
before do
sign_in(user)
end
context 'when member is not found' do
it 'returns 404' do
......@@ -115,7 +129,9 @@ describe Groups::GroupMembersController do
context 'when member is found' do
context 'and is not an owner' do
before { group.add_developer(user) }
before do
group.add_developer(user)
end
it 'removes user from members' do
delete :leave, group_id: group
......@@ -134,7 +150,9 @@ describe Groups::GroupMembersController do
end
context 'and is an owner' do
before { group.add_owner(user) }
before do
group.add_owner(user)
end
it 'cannot removes himself from the group' do
delete :leave, group_id: group
......@@ -144,7 +162,9 @@ describe Groups::GroupMembersController do
end
context 'and is a requester' do
before { group.request_access(user) }
before do
group.request_access(user)
end
it 'removes user from members' do
delete :leave, group_id: group
......@@ -159,7 +179,9 @@ describe Groups::GroupMembersController do
end
describe 'POST request_access' do
before { sign_in(user) }
before do
sign_in(user)
end
it 'creates a new GroupMember that is not a team member' do
post :request_access, group_id: group
......@@ -174,7 +196,9 @@ describe Groups::GroupMembersController do
describe 'POST approve_access_request' do
let(:member) { create(:group_member, :access_request, group: group) }
before { sign_in(user) }
before do
sign_in(user)
end
context 'when member is not found' do
it 'returns 403' do
......@@ -186,7 +210,9 @@ describe Groups::GroupMembersController do
context 'when member is found' do
context 'when user does not have enough rights' do
before { group.add_developer(user) }
before do
group.add_developer(user)
end
it 'returns 403' do
post :approve_access_request, group_id: group, id: member
......@@ -197,7 +223,9 @@ describe Groups::GroupMembersController do
end
context 'when user has enough rights' do
before { group.add_owner(user) }
before do
group.add_owner(user)
end
it 'adds user to members' do
post :approve_access_request, group_id: group, id: member
......
......@@ -94,7 +94,10 @@ describe NotificationSettingsController do
context 'not authorized' do
let(:private_project) { create(:empty_project, :private) }
before { sign_in(user) }
before do
sign_in(user)
end
it 'returns 404' do
post :create,
......@@ -120,7 +123,9 @@ describe NotificationSettingsController do
end
context 'when authorized' do
before{ sign_in(user) }
before do
sign_in(user)
end
it 'returns success' do
put :update,
......@@ -152,7 +157,9 @@ describe NotificationSettingsController do
context 'not authorized' do
let(:other_user) { create(:user) }
before { sign_in(other_user) }
before do
sign_in(other_user)
end
it 'returns 404' do
put :update,
......
......@@ -4,7 +4,9 @@ describe Profiles::PersonalAccessTokensController do
let(:user) { create(:user) }
let(:token_attributes) { attributes_for(:personal_access_token) }
before { sign_in(user) }
before do
sign_in(user)
end
describe '#create' do
def created_token
......@@ -38,7 +40,9 @@ describe Profiles::PersonalAccessTokensController do
let!(:inactive_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
let!(:impersonation_personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
before { get :index }
before do
get :index
end
it "retrieves active personal access tokens" do
expect(assigns(:active_personal_access_tokens)).to include(active_personal_access_token)
......
......@@ -281,7 +281,9 @@ describe Projects::CommitController do
end
context 'when the path does not exist in the diff' do
before { diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ) }
before do
diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ)
end
it 'returns a 404' do
expect(response).to have_http_status(404)
......@@ -302,7 +304,9 @@ describe Projects::CommitController do
end
context 'when the commit does not exist' do
before { diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path) }
before do
diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
expect(response).to have_http_status(404)
......
......@@ -128,7 +128,9 @@ describe Projects::CompareController do
end
context 'when the path does not exist in the diff' do
before { diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ) }
before do
diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ)
end
it 'returns a 404' do
expect(response).to have_http_status(404)
......@@ -149,7 +151,9 @@ describe Projects::CompareController do
end
context 'when the from ref does not exist' do
before { diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path) }
before do
diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
expect(response).to have_http_status(404)
......@@ -157,7 +161,9 @@ describe Projects::CompareController do
end
context 'when the to ref does not exist' do
before { diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path) }
before do
diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
expect(response).to have_http_status(404)
......
......@@ -14,7 +14,9 @@ describe Projects::ForksController do
end
context 'when fork is public' do
before { forked_project.update_attribute(:visibility_level, Project::PUBLIC) }
before do
forked_project.update_attribute(:visibility_level, Project::PUBLIC)
end
it 'is visible for non logged in users' do
get_forks
......@@ -35,7 +37,9 @@ describe Projects::ForksController do
end
context 'when user is logged in' do
before { sign_in(project.creator) }
before do
sign_in(project.creator)
end
context 'when user is not a Project member neither a group member' do
it 'does not see the Project listed' do
......@@ -46,7 +50,9 @@ describe Projects::ForksController do
end
context 'when user is a member of the Project' do
before { forked_project.team << [project.creator, :developer] }
before do
forked_project.team << [project.creator, :developer]
end
it 'sees the project listed' do
get_forks
......@@ -56,7 +62,9 @@ describe Projects::ForksController do
end
context 'when user is a member of the Group' do
before { forked_project.group.add_developer(project.creator) }
before do
forked_project.group.add_developer(project.creator)
end
it 'sees the project listed' do
get_forks
......
......@@ -22,7 +22,10 @@ describe Projects::GroupLinksController do
end
context 'when user has access to group he want to link project to' do
before { group.add_developer(user) }
before do
group.add_developer(user)
end
include_context 'link project to group'
it 'links project with selected group' do
......
......@@ -212,7 +212,9 @@ describe Projects::IssuesController do
let(:another_project) { create(:empty_project, :private) }
context 'when user has access to move issue' do
before { another_project.team << [user, :reporter] }
before do
another_project.team << [user, :reporter]
end
it 'moves issue to another project' do
move_issue
......@@ -250,14 +252,18 @@ describe Projects::IssuesController do
end
context 'when an issue is identified as spam' do
before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) }
before do
allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
context 'when captcha is not verified' do
def update_spam_issue
update_issue(title: 'Spam Title', description: 'Spam lives here')
end
before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) }
before do
allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
end
it 'rejects an issue recognized as a spam' do
expect { update_spam_issue }.not_to change{ issue.reload.title }
......@@ -619,14 +625,18 @@ describe Projects::IssuesController do
end
context 'when an issue is identified as spam' do
before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) }
before do
allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
context 'when captcha is not verified' do
def post_spam_issue
post_new_issue(title: 'Spam Title', description: 'Spam lives here')
end
before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) }
before do
allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
end
it 'rejects an issue recognized as a spam' do
expect { post_spam_issue }.not_to change(Issue, :count)
......@@ -738,7 +748,10 @@ describe Projects::IssuesController do
describe "DELETE #destroy" do
context "when the user is a developer" do
before { sign_in(user) }
before do
sign_in(user)
end
it "rejects a developer to destroy an issue" do
delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
expect(response).to have_http_status(404)
......@@ -750,7 +763,9 @@ describe Projects::IssuesController do
let(:namespace) { create(:namespace, owner: owner) }
let(:project) { create(:empty_project, namespace: namespace) }
before { sign_in(owner) }
before do
sign_in(owner)
end
it "deletes the issue" do
delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
......
......@@ -69,18 +69,11 @@ describe Projects::JobsController do
Ci::Build::AVAILABLE_STATUSES.each do |status|
create_build(status, status)
end
RequestStore.begin!
end
after do
RequestStore.end!
RequestStore.clear!
end
it "verifies number of queries" do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { get_index }
expect(recorded.count).to be_within(5).of(8)
expect(recorded.count).to be_within(6).of(8)
end
def create_build(name, status)
......
......@@ -49,21 +49,14 @@ describe Projects::PipelinesController do
expect(json_response['details']).to have_key 'stages'
end
context 'when the pipeline has multiple stages and groups' do
context 'when the pipeline has multiple stages and groups', :request_store do
before do
RequestStore.begin!
create_build('build', 0, 'build')
create_build('test', 1, 'rspec 0')
create_build('deploy', 2, 'production')
create_build('post deploy', 3, 'pages 0')
end
after do
RequestStore.end!
RequestStore.clear!
end
let(:project) { create(:project) }
let(:pipeline) do
create(:ci_empty_pipeline, project: project, user: user, sha: project.commit.id)
......
......@@ -16,10 +16,14 @@ describe Projects::ProjectMembersController do
describe 'POST create' do
let(:project_user) { create(:user) }
before { sign_in(user) }
before do
sign_in(user)
end
context 'when user does not have enough rights' do
before { project.team << [user, :developer] }
before do
project.team << [user, :developer]
end
it 'returns 404' do
post :create, namespace_id: project.namespace,
......@@ -33,7 +37,9 @@ describe Projects::ProjectMembersController do
end
context 'when user has enough rights' do
before { project.team << [user, :master] }
before do
project.team << [user, :master]
end
it 'adds user to members' do
expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(status: :success)
......@@ -64,7 +70,9 @@ describe Projects::ProjectMembersController do
describe 'DELETE destroy' do
let(:member) { create(:project_member, :developer, project: project) }
before { sign_in(user) }
before do
sign_in(user)
end
context 'when member is not found' do
it 'returns 404' do
......@@ -78,7 +86,9 @@ describe Projects::ProjectMembersController do
context 'when member is found' do
context 'when user does not have enough rights' do
before { project.team << [user, :developer] }
before do
project.team << [user, :developer]
end
it 'returns 404' do
delete :destroy, namespace_id: project.namespace,
......@@ -91,7 +101,9 @@ describe Projects::ProjectMembersController do
end
context 'when user has enough rights' do
before { project.team << [user, :master] }
before do
project.team << [user, :master]
end
it '[HTML] removes user from members' do
delete :destroy, namespace_id: project.namespace,
......@@ -117,7 +129,9 @@ describe Projects::ProjectMembersController do
end
describe 'DELETE leave' do
before { sign_in(user) }
before do
sign_in(user)
end
context 'when member is not found' do
it 'returns 404' do
......@@ -130,7 +144,9 @@ describe Projects::ProjectMembersController do
context 'when member is found' do
context 'and is not an owner' do
before { project.team << [user, :developer] }
before do
project.team << [user, :developer]
end
it 'removes user from members' do
delete :leave, namespace_id: project.namespace,
......@@ -145,7 +161,9 @@ describe Projects::ProjectMembersController do
context 'and is an owner' do
let(:project) { create(:empty_project, namespace: user.namespace) }
before { project.team << [user, :master] }
before do
project.team << [user, :master]
end
it 'does not remove himself from the project' do
delete :leave, namespace_id: project.namespace,
......@@ -156,7 +174,9 @@ describe Projects::ProjectMembersController do
end
context 'and is a requester' do
before { project.request_access(user) }
before do
project.request_access(user)
end
it 'removes user from members' do
delete :leave, namespace_id: project.namespace,
......@@ -172,7 +192,9 @@ describe Projects::ProjectMembersController do
end
describe 'POST request_access' do
before { sign_in(user) }
before do
sign_in(user)
end
it 'creates a new ProjectMember that is not a team member' do
post :request_access, namespace_id: project.namespace,
......@@ -190,7 +212,9 @@ describe Projects::ProjectMembersController do
describe 'POST approve' do
let(:member) { create(:project_member, :access_request, project: project) }
before { sign_in(user) }
before do
sign_in(user)
end
context 'when member is not found' do
it 'returns 404' do
......@@ -204,7 +228,9 @@ describe Projects::ProjectMembersController do
context 'when member is found' do
context 'when user does not have enough rights' do
before { project.team << [user, :developer] }
before do
project.team << [user, :developer]
end
it 'returns 404' do
post :approve_access_request, namespace_id: project.namespace,
......@@ -217,7 +243,9 @@ describe Projects::ProjectMembersController do
end
context 'when user has enough rights' do
before { project.team << [user, :master] }
before do
project.team << [user, :master]
end
it 'adds user to members' do
post :approve_access_request, namespace_id: project.namespace,
......@@ -252,7 +280,10 @@ describe Projects::ProjectMembersController do
end
context 'when user can access source project members' do
before { another_project.team << [user, :guest] }
before do
another_project.team << [user, :guest]
end
include_context 'import applied'
it 'imports source project members' do
......
......@@ -46,7 +46,9 @@ describe Projects::SnippetsController do
end
context 'when signed in as the author' do
before { sign_in(user) }
before do
sign_in(user)
end
it 'renders the snippet' do
get :index, namespace_id: project.namespace, project_id: project
......@@ -57,7 +59,9 @@ describe Projects::SnippetsController do
end
context 'when signed in as a project member' do
before { sign_in(user2) }
before do
sign_in(user2)
end
it 'renders the snippet' do
get :index, namespace_id: project.namespace, project_id: project
......@@ -317,7 +321,9 @@ describe Projects::SnippetsController do
end
context 'when signed in as the author' do
before { sign_in(user) }
before do
sign_in(user)
end
it 'renders the snippet' do
get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
......@@ -328,7 +334,9 @@ describe Projects::SnippetsController do
end
context 'when signed in as a project member' do
before { sign_in(user2) }
before do
sign_in(user2)
end
it 'renders the snippet' do
get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
......@@ -349,7 +357,9 @@ describe Projects::SnippetsController do
end
context 'when signed in' do
before { sign_in(user) }
before do
sign_in(user)
end
it 'responds with status 404' do
get action, namespace_id: project.namespace, project_id: project, id: 42
......
......@@ -6,7 +6,9 @@ describe Projects::TagsController do
let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') }
describe 'GET index' do
before { get :index, namespace_id: project.namespace.to_param, project_id: project }
before do
get :index, namespace_id: project.namespace.to_param, project_id: project
end
it 'returns the tags for the page' do
expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0'])
......@@ -19,7 +21,9 @@ describe Projects::TagsController do
end
describe 'GET show' do
before { get :show, namespace_id: project.namespace.to_param, project_id: project, id: id }
before do
get :show, namespace_id: project.namespace.to_param, project_id: project, id: id
end
context "valid tag" do
let(:id) { 'v1.0.0' }
......
......@@ -29,7 +29,9 @@ describe ProjectsController do
describe "GET show" do
context "user not project member" do
before { sign_in(user) }
before do
sign_in(user)
end
context "user does not have access to project" do
let(:private_project) { create(:empty_project, :private) }
......@@ -108,7 +110,9 @@ describe ProjectsController do
context "project with empty repo" do
let(:empty_project) { create(:project_empty_repo, :public) }
before { sign_in(user) }
before do
sign_in(user)
end
User.project_views.keys.each do |project_view|
context "with #{project_view} view set" do
......@@ -128,7 +132,9 @@ describe ProjectsController do
context "project with broken repo" do
let(:empty_project) { create(:project_broken_repo, :public) }
before { sign_in(user) }
before do
sign_in(user)
end
User.project_views.keys.each do |project_view|
context "with #{project_view} view set" do
......
......@@ -18,7 +18,9 @@ describe SearchController do
context 'on restricted projects' do
context 'when signed out' do
before { sign_out(user) }
before do
sign_out(user)
end
it "doesn't expose comments on issues" do
project = create(:empty_project, :public, :issues_private)
......
......@@ -14,7 +14,9 @@ describe SentNotificationsController, type: :controller do
describe 'GET unsubscribe' do
context 'when the user is not logged in' do
context 'when the force param is passed' do
before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
before do
get(:unsubscribe, id: sent_notification.reply_key, force: true)
end
it 'unsubscribes the user' do
expect(issue.subscribed?(user, project)).to be_falsey
......@@ -30,7 +32,9 @@ describe SentNotificationsController, type: :controller do
end
context 'when the force param is not passed' do
before { get(:unsubscribe, id: sent_notification.reply_key) }
before do
get(:unsubscribe, id: sent_notification.reply_key)
end
it 'does not unsubscribe the user' do
expect(issue.subscribed?(user, project)).to be_truthy
......@@ -47,10 +51,14 @@ describe SentNotificationsController, type: :controller do
end
context 'when the user is logged in' do
before { sign_in(user) }
before do
sign_in(user)
end
context 'when the ID passed does not exist' do
before { get(:unsubscribe, id: sent_notification.reply_key.reverse) }
before do
get(:unsubscribe, id: sent_notification.reply_key.reverse)
end
it 'does not unsubscribe the user' do
expect(issue.subscribed?(user, project)).to be_truthy
......@@ -66,7 +74,9 @@ describe SentNotificationsController, type: :controller do
end
context 'when the force param is passed' do
before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
before do
get(:unsubscribe, id: sent_notification.reply_key, force: true)
end
it 'unsubscribes the user' do
expect(issue.subscribed?(user, project)).to be_falsey
......@@ -89,7 +99,10 @@ describe SentNotificationsController, type: :controller do
end
end
let(:sent_notification) { create(:sent_notification, project: project, noteable: merge_request, recipient: user) }
before { get(:unsubscribe, id: sent_notification.reply_key) }
before do
get(:unsubscribe, id: sent_notification.reply_key)
end
it 'unsubscribes the user' do
expect(merge_request.subscribed?(user, project)).to be_falsey
......
......@@ -142,7 +142,9 @@ describe SessionsController do
end
context 'when OTP is invalid' do
before { authenticate_2fa(otp_attempt: 'invalid') }
before do
authenticate_2fa(otp_attempt: 'invalid')
end
it 'does not authenticate' do
expect(subject.current_user).not_to eq user
......@@ -169,7 +171,9 @@ describe SessionsController do
end
context 'when OTP is invalid' do
before { authenticate_2fa(otp_attempt: 'invalid') }
before do
authenticate_2fa(otp_attempt: 'invalid')
end
it 'does not authenticate' do
expect(subject.current_user).not_to eq user
......
......@@ -437,7 +437,9 @@ describe SnippetsController do
end
context 'when signed in user is the author' do
before { get :raw, id: personal_snippet.to_param }
before do
get :raw, id: personal_snippet.to_param
end
it 'responds with status 200' do
expect(assigns(:snippet)).to eq(personal_snippet)
......
......@@ -43,7 +43,9 @@ describe UsersController do
end
context 'when logged in' do
before { sign_in(user) }
before do
sign_in(user)
end
it 'renders show' do
get :show, username: user.username
......@@ -62,7 +64,9 @@ describe UsersController do
end
context 'when logged in' do
before { sign_in(user) }
before do
sign_in(user)
end
it 'renders 404' do
get :show, username: 'nonexistent'
......
FactoryGirl.define do
factory :issue_link do
source factory: :issue
target factory: :issue
end
end
......@@ -134,7 +134,10 @@ describe "Admin Runners" do
describe 'runners registration token' do
let!(:token) { current_application_settings.runners_registration_token }
before { visit admin_runners_path }
before do
visit admin_runners_path
end
it 'has a registration token' do
expect(page).to have_content("Registration token is #{token}")
......
......@@ -12,7 +12,9 @@ describe 'Admin > Users > Impersonation Tokens', feature: true, js: true do
find(".table.inactive-tokens")
end
before { login_as(admin) }
before do
login_as(admin)
end
describe "token creation" do
it "allows creation of a token" do
......
......@@ -124,7 +124,10 @@ describe "Admin::Users", feature: true do
describe 'Impersonation' do
let(:another_user) { create(:user) }
before { visit admin_user_path(another_user) }
before do
visit admin_user_path(another_user)
end
context 'before impersonating' do
it 'shows impersonate button for other users' do
......@@ -149,7 +152,9 @@ describe "Admin::Users", feature: true do
end
context 'when impersonating' do
before { click_link 'Impersonate' }
before do
click_link 'Impersonate'
end
it 'logs in as the user when impersonate is clicked' do
expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(another_user.username)
......
......@@ -12,7 +12,9 @@ describe 'Dashboard Merge Requests' do
end
describe 'new merge request dropdown' do
before { visit merge_requests_dashboard_path }
before do
visit merge_requests_dashboard_path
end
it 'shows projects only with merge requests feature enabled', js: true do
find('.new-project-item-select-button').trigger('click')
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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