Commit fd5fdb2c authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into bootstrap4

parents 42189e91 ec7163ae
...@@ -189,7 +189,7 @@ stages: ...@@ -189,7 +189,7 @@ stages:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg <<: *use-pg
variables: variables:
CREATE_DB_USER: "true" SETUP_DB: "false"
script: script:
# Manually clone gitlab-test and only seed this project in # Manually clone gitlab-test and only seed this project in
# db/fixtures/development/04_project.rb thanks to SIZE=1 below # db/fixtures/development/04_project.rb thanks to SIZE=1 below
...@@ -233,7 +233,7 @@ stages: ...@@ -233,7 +233,7 @@ stages:
.migration-paths: &migration-paths .migration-paths: &migration-paths
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables: variables:
CREATE_DB_USER: "true" SETUP_DB: "false"
script: script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0 - git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD - git checkout -f FETCH_HEAD
...@@ -242,7 +242,7 @@ stages: ...@@ -242,7 +242,7 @@ stages:
- cp config/gitlab.yml.example config/gitlab.yml - cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu - bundle exec rake db:drop db:create db:schema:load db:seed_fu
- date - date
- git checkout $CI_COMMIT_SHA - git checkout -f $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS - bundle install $BUNDLE_INSTALL_FLAGS
- date - date
- . scripts/prepare_build.sh - . scripts/prepare_build.sh
......
...@@ -93,10 +93,13 @@ export default { ...@@ -93,10 +93,13 @@ export default {
v-html="actionTextHtml" v-html="actionTextHtml"
class="system-note-message"> class="system-note-message">
</span> </span>
<span class="system-note-separator">
&middot;
</span>
<a <a
:href="noteTimestampLink" :href="noteTimestampLink"
@click="updateTargetNoteHash" @click="updateTargetNoteHash"
class="note-timestamp"> class="note-timestamp system-note-separator">
<time-ago-tooltip <time-ago-tooltip
:time="createdAt" :time="createdAt"
tooltip-placement="bottom" tooltip-placement="bottom"
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ /* eslint-disable func-names, space-before-function-paren, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
...@@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats'; ...@@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats';
const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse };
const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
const hasProp = {}.hasOwnProperty; const hasProp = {}.hasOwnProperty;
const extend = function(child, parent) { for (const key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
export const ContributorsGraph = (function() { export const ContributorsGraph = (function() {
function ContributorsGraph() {} function ContributorsGraph() {}
ContributorsGraph.prototype.MARGIN = { ContributorsGraph.prototype.MARGIN = {
top: 20, top: 20,
right: 20, right: 10,
bottom: 30, bottom: 30,
left: 50 left: 40
}; };
ContributorsGraph.prototype.x_domain = null; ContributorsGraph.prototype.x_domain = null;
...@@ -32,6 +32,12 @@ export const ContributorsGraph = (function() { ...@@ -32,6 +32,12 @@ export const ContributorsGraph = (function() {
ContributorsGraph.prototype.dates = []; ContributorsGraph.prototype.dates = [];
ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) {
const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
const marginWidth = this.MARGIN.left + this.MARGIN.right;
return baseWidth - parentPaddingWidth - marginWidth;
};
ContributorsGraph.set_x_domain = function(data) { ContributorsGraph.set_x_domain = function(data) {
return ContributorsGraph.prototype.x_domain = data; return ContributorsGraph.prototype.x_domain = data;
}; };
...@@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) {
function ContributorsMasterGraph(data1) { function ContributorsMasterGraph(data1) {
const $parentElement = $('#contributors-master'); const $parentElement = $('#contributors-master');
const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
this.data = data1; this.data = data1;
this.update_content = this.update_content.bind(this); this.update_content = this.update_content.bind(this);
this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right); this.width = this.determine_width($('.js-graphs-show').width(), $parentElement);
this.height = 200; this.height = 200;
this.x = null; this.x = null;
this.y = null; this.y = null;
...@@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) {
} }
ContributorsMasterGraph.prototype.process_dates = function(data) { ContributorsMasterGraph.prototype.process_dates = function(data) {
var dates; const dates = this.get_dates(data);
dates = this.get_dates(data);
this.parse_dates(data); this.parse_dates(data);
return ContributorsGraph.set_dates(dates); return ContributorsGraph.set_dates(dates);
}; };
...@@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) {
}; };
ContributorsMasterGraph.prototype.parse_dates = function(data) { ContributorsMasterGraph.prototype.parse_dates = function(data) {
var parseDate; const parseDate = d3.timeParse("%Y-%m-%d");
parseDate = d3.timeParse("%Y-%m-%d");
return data.forEach(function(d) { return data.forEach(function(d) {
return d.date = parseDate(d.date); return d.date = parseDate(d.date);
}); });
...@@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) {
}; };
ContributorsMasterGraph.prototype.create_svg = function() { ContributorsMasterGraph.prototype.create_svg = function() {
return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); this.svg = d3.select("#contributors-master")
.append("svg")
.attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
.attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
.attr("class", "tint-box")
.append("g")
.attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
return this.svg;
}; };
ContributorsMasterGraph.prototype.create_area = function(x, y) { ContributorsMasterGraph.prototype.create_area = function(x, y) {
...@@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) { ...@@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) {
extend(ContributorsAuthorGraph, superClass); extend(ContributorsAuthorGraph, superClass);
function ContributorsAuthorGraph(data1) { function ContributorsAuthorGraph(data1) {
const $parentElements = $('.person');
this.data = data1; this.data = data1;
// Don't split graph size in half for mobile devices. // Don't split graph size in half for mobile devices.
if ($(window).width() < 768) { if ($(window).width() < 790) {
this.width = $('.content').width() - 80; this.width = this.determine_width($('.js-graphs-show').width(), $parentElements);
} else { } else {
this.width = ($('.content').width() / 2) - 100; this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements);
} }
this.height = 200; this.height = 200;
this.x = null; this.x = null;
...@@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) { ...@@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) {
ContributorsAuthorGraph.prototype.create_area = function(x, y) { ContributorsAuthorGraph.prototype.create_area = function(x, y) {
return this.area = d3.area().x(function(d) { return this.area = d3.area().x(function(d) {
var parseDate; const parseDate = d3.timeParse("%Y-%m-%d");
parseDate = d3.timeParse("%Y-%m-%d");
return x(parseDate(d)); return x(parseDate(d));
}).y0(this.height).y1((function(_this) { }).y0(this.height).y1((function(_this) {
return function(d) { return function(d) {
...@@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) { ...@@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) {
}; };
ContributorsAuthorGraph.prototype.create_svg = function() { ContributorsAuthorGraph.prototype.create_svg = function() {
var persons = document.querySelectorAll('.person'); const persons = document.querySelectorAll('.person');
this.list_item = persons[persons.length - 1]; this.list_item = persons[persons.length - 1];
return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); this.svg = d3.select(this.list_item)
.append("svg")
.attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
.attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
.attr("class", "spark")
.append("g")
.attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
return this.svg;
}; };
ContributorsAuthorGraph.prototype.draw_path = function(data) { ContributorsAuthorGraph.prototype.draw_path = function(data) {
......
...@@ -40,7 +40,7 @@ export default { ...@@ -40,7 +40,7 @@ export default {
:class="cssClass" :class="cssClass"
:title="tooltipTitle(time)" :title="tooltipTitle(time)"
:data-placement="tooltipPlacement" :data-placement="tooltipPlacement"
data-container="body"> data-container="body"
{{ timeFormated(time) }} v-text="timeFormated(time)">
</time> </time>
</template> </template>
@import './issues/issue_count_badge';
[v-cloak] { [v-cloak] {
display: none; display: none;
} }
......
@import "./issues/issue_count_badge";
.issues-list { .issues-list {
.issue { .issue {
padding: 10px 0 10px $gl-padding; padding: 10px 0 10px $gl-padding;
......
...@@ -455,6 +455,10 @@ ul.notes { ...@@ -455,6 +455,10 @@ ul.notes {
white-space: normal; white-space: normal;
} }
.system-note-separator {
color: $gl-text-color-disabled;
}
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
......
class Admin::DashboardController < Admin::ApplicationController class Admin::DashboardController < Admin::ApplicationController
include CountHelper
def index def index
@projects = Project.order_id_desc.without_deleted.with_route.limit(10) @projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10) @users = User.order_id_desc.limit(10)
......
module AcceptsPendingInvitations
extend ActiveSupport::Concern
def accept_pending_invitations
return unless resource.active_for_authentication?
clear_stored_location_for_resource if resource.accept_pending_invitations!.any?
end
def clear_stored_location_for_resource
session_key = stored_location_key_for(resource)
session.delete(session_key)
end
end
class ConfirmationsController < Devise::ConfirmationsController class ConfirmationsController < Devise::ConfirmationsController
include AcceptsPendingInvitations
def almost_there def almost_there
flash[:notice] = nil flash[:notice] = nil
render layout: "devise_empty" render layout: "devise_empty"
...@@ -11,6 +13,8 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -11,6 +13,8 @@ class ConfirmationsController < Devise::ConfirmationsController
end end
def after_confirmation_path_for(resource_name, resource) def after_confirmation_path_for(resource_name, resource)
accept_pending_invitations
# incoming resource can either be a :user or an :email # incoming resource can either be a :user or an :email
if signed_in?(:user) if signed_in?(:user)
after_sign_in(resource) after_sign_in(resource)
......
...@@ -23,8 +23,12 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -23,8 +23,12 @@ class Projects::CommitController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html { render } format.html { render }
format.diff { render text: @commit.to_diff } format.diff do
format.patch { render text: @commit.to_patch } send_git_diff(@project.repository, @commit.diff_refs)
end
format.patch do
send_git_patch(@project.repository, @commit.diff_refs)
end
end end
end end
......
...@@ -69,7 +69,7 @@ module Projects ...@@ -69,7 +69,7 @@ module Projects
@project_runners = @project.runners.ordered @project_runners = @project.runners.ordered
@assignable_runners = current_user @assignable_runners = current_user
.ci_authorized_runners .ci_owned_runners
.assignable_for(project) .assignable_for(project)
.ordered .ordered
.page(params[:page]).per(20) .page(params[:page]).per(20)
......
class RegistrationsController < Devise::RegistrationsController class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify include Recaptcha::Verify
include AcceptsPendingInvitations
before_action :whitelist_query_limiting, only: [:destroy] before_action :whitelist_query_limiting, only: [:destroy]
...@@ -16,6 +17,7 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -16,6 +17,7 @@ class RegistrationsController < Devise::RegistrationsController
end end
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
accept_pending_invitations
super super
else else
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
...@@ -60,7 +62,7 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -60,7 +62,7 @@ class RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(user) def after_sign_up_path_for(user)
Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}") Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}")
user.confirmed? ? dashboard_projects_path : users_almost_there_path user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path
end end
def after_inactive_sign_up_path_for(resource) def after_inactive_sign_up_path_for(resource)
......
module CountHelper
def approximate_count_with_delimiters(model)
number_with_delimiter(Gitlab::Database::Count.approximate_count(model))
end
end
...@@ -2,6 +2,7 @@ class Appearance < ActiveRecord::Base ...@@ -2,6 +2,7 @@ class Appearance < ActiveRecord::Base
include CacheMarkdownField include CacheMarkdownField
include AfterCommitQueue include AfterCommitQueue
include ObjectStorage::BackgroundMove include ObjectStorage::BackgroundMove
include WithUploads
cache_markdown_field :description cache_markdown_field :description
cache_markdown_field :new_project_guidelines cache_markdown_field :new_project_guidelines
...@@ -14,8 +15,6 @@ class Appearance < ActiveRecord::Base ...@@ -14,8 +15,6 @@ class Appearance < ActiveRecord::Base
mount_uploader :logo, AttachmentUploader mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
CACHE_KEY = "current_appearance:#{Gitlab::VERSION}".freeze CACHE_KEY = "current_appearance:#{Gitlab::VERSION}".freeze
after_commit :flush_redis_cache after_commit :flush_redis_cache
......
...@@ -52,7 +52,7 @@ module Ci ...@@ -52,7 +52,7 @@ module Ci
# Without that, placeholders would miss one and couldn't match. # Without that, placeholders would miss one and couldn't match.
where(locked: false) where(locked: false)
.where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})") .where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})")
.specific .project_type
end end
validate :tag_constraints validate :tag_constraints
......
# Mounted uploaders are destroyed by carrierwave's after_commit
# hook. This hook fetches upload location (local vs remote) from
# Upload model. So it's neccessary to make sure that during that
# after_commit hook model's associated uploads are not deleted yet.
# IOW we can not use dependent: :destroy :
# has_many :uploads, as: :model, dependent: :destroy
#
# And because not-mounted uploads require presence of upload's
# object model when destroying them (FileUploader's `build_upload` method
# references `model` on delete), we can not use after_commit hook for these
# uploads.
#
# Instead FileUploads are destroyed in before_destroy hook and remaining uploads
# are destroyed by the carrierwave's after_commit hook.
module WithUploads
extend ActiveSupport::Concern
# Currently there is no simple way how to select only not-mounted
# uploads, it should be all FileUploaders so we select them by
# `uploader` class
FILE_UPLOADERS = %w(PersonalFileUploader NamespaceFileUploader FileUploader).freeze
included do
has_many :uploads, as: :model
before_destroy :destroy_file_uploads
end
# mounted uploads are deleted in carrierwave's after_commit hook,
# but FileUploaders which are not mounted must be deleted explicitly and
# it can not be done in after_commit because FileUploader requires loads
# associated model on destroy (which is already deleted in after_commit)
def destroy_file_uploads
self.uploads.where(uploader: FILE_UPLOADERS).find_each do |upload|
upload.destroy
end
end
end
...@@ -10,6 +10,7 @@ class Group < Namespace ...@@ -10,6 +10,7 @@ class Group < Namespace
include LoadedInGroupList include LoadedInGroupList
include GroupDescendant include GroupDescendant
include TokenAuthenticatable include TokenAuthenticatable
include WithUploads
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members alias_method :members, :group_members
...@@ -30,8 +31,6 @@ class Group < Namespace ...@@ -30,8 +31,6 @@ class Group < Namespace
has_many :variables, class_name: 'Ci::GroupVariable' has_many :variables, class_name: 'Ci::GroupVariable'
has_many :custom_attributes, class_name: 'GroupCustomAttribute' has_many :custom_attributes, class_name: 'GroupCustomAttribute'
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :boards has_many :boards
has_many :badges, class_name: 'GroupBadge' has_many :badges, class_name: 'GroupBadge'
......
...@@ -23,6 +23,7 @@ class Project < ActiveRecord::Base ...@@ -23,6 +23,7 @@ class Project < ActiveRecord::Base
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
include ChronicDurationAttribute include ChronicDurationAttribute
include FastDestroyAll::Helpers include FastDestroyAll::Helpers
include WithUploads
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
...@@ -301,8 +302,6 @@ class Project < ActiveRecord::Base ...@@ -301,8 +302,6 @@ class Project < ActiveRecord::Base
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
validates :variables, variable_duplicates: { scope: :environment_scope } validates :variables, variable_duplicates: { scope: :environment_scope }
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Scopes # Scopes
scope :pending_delete, -> { where(pending_delete: true) } scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) } scope :without_deleted, -> { where(pending_delete: false) }
......
...@@ -17,6 +17,7 @@ class User < ActiveRecord::Base ...@@ -17,6 +17,7 @@ class User < ActiveRecord::Base
include IgnorableColumn include IgnorableColumn
include BulkMemberAccessLoad include BulkMemberAccessLoad
include BlocksJsonSerialization include BlocksJsonSerialization
include WithUploads
DEFAULT_NOTIFICATION_LEVEL = :participating DEFAULT_NOTIFICATION_LEVEL = :participating
...@@ -137,7 +138,6 @@ class User < ActiveRecord::Base ...@@ -137,7 +138,6 @@ class User < ActiveRecord::Base
has_many :custom_attributes, class_name: 'UserCustomAttribute' has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout' has_many :callouts, class_name: 'UserCallout'
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :term_agreements has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term' belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
...@@ -860,6 +860,16 @@ class User < ActiveRecord::Base ...@@ -860,6 +860,16 @@ class User < ActiveRecord::Base
confirmed? && !temp_oauth_email? confirmed? && !temp_oauth_email?
end end
def accept_pending_invitations!
pending_invitations.select do |member|
member.accept_invite!(self)
end
end
def pending_invitations
Member.where(invite_email: verified_emails).invite
end
def all_emails def all_emails
all_emails = [] all_emails = []
all_emails << email unless temp_oauth_email? all_emails << email unless temp_oauth_email?
...@@ -999,12 +1009,19 @@ class User < ActiveRecord::Base ...@@ -999,12 +1009,19 @@ class User < ActiveRecord::Base
!solo_owned_groups.present? !solo_owned_groups.present?
end end
def ci_authorized_runners def ci_owned_runners
@ci_authorized_runners ||= begin @ci_owned_runners ||= begin
runner_ids = Ci::RunnerProject project_runner_ids = Ci::RunnerProject
.where(project: authorized_projects(Gitlab::Access::MASTER)) .where(project: authorized_projects(Gitlab::Access::MASTER))
.select(:runner_id) .select(:runner_id)
Ci::Runner.specific.where(id: runner_ids)
group_runner_ids = Ci::RunnerNamespace
.where(namespace_id: owned_or_masters_groups.select(:id))
.select(:runner_id)
union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids])
Ci::Runner.specific.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end end
end end
...@@ -1205,6 +1222,11 @@ class User < ActiveRecord::Base ...@@ -1205,6 +1222,11 @@ class User < ActiveRecord::Base
!terms_accepted? !terms_accepted?
end end
def owned_or_masters_groups
union = Gitlab::SQL::Union.new([owned_groups, masters_groups])
Group.from("(#{union.to_sql}) namespaces")
end
protected protected
# override, from Devise::Validatable # override, from Devise::Validatable
......
module Ci module Ci
class RunnerPolicy < BasePolicy class RunnerPolicy < BasePolicy
with_options scope: :subject, score: 0
condition(:shared) { @subject.is_shared? }
with_options scope: :subject, score: 0 with_options scope: :subject, score: 0
condition(:locked, scope: :subject) { @subject.locked? } condition(:locked, scope: :subject) { @subject.locked? }
condition(:authorized_runner) { @user.ci_authorized_runners.include?(@subject) } condition(:owned_runner) { @user.ci_owned_runners.exists?(@subject.id) }
rule { anonymous }.prevent_all rule { anonymous }.prevent_all
rule { admin | authorized_runner }.enable :assign_runner
rule { ~admin & shared }.prevent :assign_runner rule { admin | owned_runner }.policy do
enable :assign_runner
enable :read_runner
enable :update_runner
enable :delete_runner
end
rule { ~admin & locked }.prevent :assign_runner rule { ~admin & locked }.prevent :assign_runner
end end
end end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
= link_to admin_projects_path do = link_to admin_projects_path do
%h3.text-center %h3.text-center
Projects: Projects:
= number_with_delimiter(Project.cached_count) = approximate_count_with_delimiters(Project)
%hr %hr
= link_to('New project', new_project_path, class: "btn btn-new") = link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4 .col-sm-4
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
= link_to admin_users_path do = link_to admin_users_path do
%h3.text-center %h3.text-center
Users: Users:
= number_with_delimiter(User.count) = approximate_count_with_delimiters(User)
%hr %hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new" = link_to 'New user', new_admin_user_path, class: "btn btn-new"
.col-sm-4 .col-sm-4
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
= link_to admin_groups_path do = link_to admin_groups_path do
%h3.text-center %h3.text-center
Groups: Groups:
= number_with_delimiter(Group.count) = approximate_count_with_delimiters(Group)
%hr %hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new" = link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row .row
...@@ -39,31 +39,31 @@ ...@@ -39,31 +39,31 @@
%p %p
Forks Forks
%span.light.float-right %span.light.float-right
= number_with_delimiter(ForkedProjectLink.count) = approximate_count_with_delimiters(ForkedProjectLink)
%p %p
Issues Issues
%span.light.float-right %span.light.float-right
= number_with_delimiter(Issue.count) = approximate_count_with_delimiters(Issue)
%p %p
Merge Requests Merge Requests
%span.light.float-right %span.light.float-right
= number_with_delimiter(MergeRequest.count) = approximate_count_with_delimiters(MergeRequest)
%p %p
Notes Notes
%span.light.float-right %span.light.float-right
= number_with_delimiter(Note.count) = approximate_count_with_delimiters(Note)
%p %p
Snippets Snippets
%span.light.float-right %span.light.float-right
= number_with_delimiter(Snippet.count) = approximate_count_with_delimiters(Snippet)
%p %p
SSH Keys SSH Keys
%span.light.float-right %span.light.float-right
= number_with_delimiter(Key.count) = approximate_count_with_delimiters(Key)
%p %p
Milestones Milestones
%span.light.float-right %span.light.float-right
= number_with_delimiter(Milestone.count) = approximate_count_with_delimiters(Milestone)
%p %p
Active Users Active Users
%span.light.float-right %span.light.float-right
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
by by
= link_to member.created_by.name, user_url(member.created_by) = link_to member.created_by.name, user_url(member.created_by)
to join the to join the
= link_to member_source.human_name, member_source.web_url = link_to member_source.human_name, member_source.public? ? member_source.web_url : invite_url(@token)
#{member_source.model_name.singular} as #{member.human_access}. #{member_source.model_name.singular} as #{member.human_access}.
%p %p
......
...@@ -41,8 +41,9 @@ ...@@ -41,8 +41,9 @@
- if note.system - if note.system
%span.system-note-message %span.system-note-message
= markdown_field(note, :note) = markdown_field(note, :note)
%a{ href: "##{dom_id(note)}" } %span.system-note-separator
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') &middot;
%a.system-note-separator{ href: "##{dom_id(note)}" }= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system? - unless note.system?
.note-actions .note-actions
- if note.for_personal_snippet? - if note.for_personal_snippet?
......
---
title: Fix width of contributors graphs
merge_request: 18639
author: Paul Vorbach
type: fixed
---
title: Automatically accepts project/group invite by email after user signup
merge_request: 17634
author: Jacopo Beschi @jacopo-beschi
type: changed
---
title: Add dot to separate system notes content
merge_request: 18864
author:
type: changed
---
title: Fix deletion of Object Store uploads
merge_request:
author:
type: fixed
---
title: Workhorse to send raw diff and patch for commits
merge_request:
author:
type: other
...@@ -165,6 +165,7 @@ module API ...@@ -165,6 +165,7 @@ module API
group = find_group!(params[:id]) group = find_group!(params[:id])
authorize! :admin_group, group authorize! :admin_group, group
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
destroy_conditionally!(group) do |group| destroy_conditionally!(group) do |group|
::Groups::DestroyService.new(group, current_user).execute ::Groups::DestroyService.new(group, current_user).execute
end end
......
...@@ -14,7 +14,7 @@ module API ...@@ -14,7 +14,7 @@ module API
use :pagination use :pagination
end end
get do get do
runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: %w(specific shared)) runners = filter_runners(current_user.ci_owned_runners, params[:scope], without: %w(specific shared))
present paginate(runners), with: Entities::Runner present paginate(runners), with: Entities::Runner
end end
...@@ -184,40 +184,35 @@ module API ...@@ -184,40 +184,35 @@ module API
def authenticate_show_runner!(runner) def authenticate_show_runner!(runner)
return if runner.is_shared || current_user.admin? return if runner.is_shared || current_user.admin?
forbidden!("No access granted") unless user_can_access_runner?(runner) forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
end end
def authenticate_update_runner!(runner) def authenticate_update_runner!(runner)
return if current_user.admin? return if current_user.admin?
forbidden!("Runner is shared") if runner.is_shared? forbidden!("No access granted") unless can?(current_user, :update_runner, runner)
forbidden!("No access granted") unless user_can_access_runner?(runner)
end end
def authenticate_delete_runner!(runner) def authenticate_delete_runner!(runner)
return if current_user.admin? return if current_user.admin?
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner associated with more than one project") if runner.projects.count > 1 forbidden!("Runner associated with more than one project") if runner.projects.count > 1
forbidden!("No access granted") unless user_can_access_runner?(runner) forbidden!("No access granted") unless can?(current_user, :delete_runner, runner)
end end
def authenticate_enable_runner!(runner) def authenticate_enable_runner!(runner)
forbidden!("Runner is shared") if runner.is_shared? forbidden!("Runner is a group runner") if runner.group_type?
forbidden!("Runner is locked") if runner.locked?
return if current_user.admin? return if current_user.admin?
forbidden!("No access granted") unless user_can_access_runner?(runner) forbidden!("Runner is locked") if runner.locked?
forbidden!("No access granted") unless can?(current_user, :assign_runner, runner)
end end
def authenticate_list_runners_jobs!(runner) def authenticate_list_runners_jobs!(runner)
return if current_user.admin? return if current_user.admin?
forbidden!("No access granted") unless user_can_access_runner?(runner) forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
end
def user_can_access_runner?(runner)
current_user.ci_authorized_runners.exists?(runner.id)
end end
end end
end end
......
...@@ -131,6 +131,7 @@ module API ...@@ -131,6 +131,7 @@ module API
delete ":id" do delete ":id" do
group = find_group!(params[:id]) group = find_group!(params[:id])
authorize! :admin_group, group authorize! :admin_group, group
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
end end
......
...@@ -58,7 +58,7 @@ module API ...@@ -58,7 +58,7 @@ module API
end end
def user_can_access_runner?(runner) def user_can_access_runner?(runner)
current_user.ci_authorized_runners.exists?(runner.id) current_user.ci_owned_runners.exists?(runner.id)
end end
end end
end end
......
# For large tables, PostgreSQL can take a long time to count rows due to MVCC.
# We can optimize this by using the reltuples count as described in https://wiki.postgresql.org/wiki/Slow_Counting.
module Gitlab
module Database
module Count
CONNECTION_ERRORS =
if defined?(PG)
[
ActionView::Template::Error,
ActiveRecord::StatementInvalid,
PG::Error
].freeze
else
[
ActionView::Template::Error,
ActiveRecord::StatementInvalid
].freeze
end
def self.approximate_count(model)
return model.count unless Gitlab::Database.postgresql?
execute_estimate_if_updated_recently(model) || model.count
end
def self.execute_estimate_if_updated_recently(model)
ActiveRecord::Base.connection.select_value(postgresql_estimate_query(model)).to_i if reltuples_updated_recently?(model)
rescue *CONNECTION_ERRORS
end
def self.reltuples_updated_recently?(model)
time = "to_timestamp(#{1.hour.ago.to_i})"
query = <<~SQL
SELECT 1 FROM pg_stat_user_tables WHERE relname = '#{model.table_name}' AND
(last_vacuum > #{time} OR last_autovacuum > #{time} OR last_analyze > #{time} OR last_autoanalyze > #{time})
SQL
ActiveRecord::Base.connection.select_all(query).count > 0
rescue *CONNECTION_ERRORS
false
end
def self.postgresql_estimate_query(model)
"SELECT reltuples::bigint AS estimate FROM pg_class where relname = '#{model.table_name}'"
end
end
end
end
...@@ -342,21 +342,6 @@ module Gitlab ...@@ -342,21 +342,6 @@ module Gitlab
parent_ids.first parent_ids.first
end end
# Shows the diff between the commit's parent and the commit.
#
# Cuts out the header and stats from #to_patch and returns only the diff.
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324
def to_diff
Gitlab::GitalyClient.migrate(:commit_patch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
@repository.gitaly_commit_client.patch(id)
else
rugged_diff_from_parent.patch
end
end
end
# Returns a diff object for the changes from this commit's first parent. # Returns a diff object for the changes from this commit's first parent.
# If there is no parent, then the diff is between this commit and an # If there is no parent, then the diff is between this commit and an
# empty repo. See Repository#diff for keys allowed in the +options+ # empty repo. See Repository#diff for keys allowed in the +options+
...@@ -432,16 +417,6 @@ module Gitlab ...@@ -432,16 +417,6 @@ module Gitlab
Gitlab::Git::CommitStats.new(@repository, self) Gitlab::Git::CommitStats.new(@repository, self)
end end
def to_patch(options = {})
begin
rugged_commit.to_mbox(options)
rescue Rugged::InvalidError => ex
if ex.message =~ /commit \w+ is a merge commit/i
'Patch format is not currently supported for merge commits.'
end
end
end
# Get ref names collection # Get ref names collection
# #
# Ex. # Ex.
......
#!/bin/bash #!/bin/bash
mysql --user=root --host=mysql <<EOF mysql --user=root --host=mysql <<EOF
CREATE DATABASE IF NOT EXISTS gitlabhq_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'gitlab'@'%'; CREATE USER IF NOT EXISTS 'gitlab'@'%';
GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%'; GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
FLUSH PRIVILEGES; FLUSH PRIVILEGES;
......
#!/bin/bash #!/bin/bash
psql -h postgres -U postgres postgres <<EOF psql -h postgres -U postgres postgres <<EOF
DROP DATABASE IF EXISTS gitlabhq_test;
CREATE DATABASE gitlabhq_test;
CREATE USER gitlab; CREATE USER gitlab;
GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gitlab;
EOF EOF
...@@ -49,20 +49,8 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml ...@@ -49,20 +49,8 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml
cp config/redis.shared_state.yml.example config/redis.shared_state.yml cp config/redis.shared_state.yml.example config/redis.shared_state.yml
sed -i 's/localhost/redis/g' config/redis.shared_state.yml sed -i 's/localhost/redis/g' config/redis.shared_state.yml
# Some tasks (e.g. db:seed_fu) need to have a properly-configured database
# user but not necessarily a full schema loaded
if [ "$CREATE_DB_USER" != "false" ]; then
if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
. scripts/create_postgres_user.sh
else
. scripts/create_mysql_user.sh
fi
fi
if [ "$SETUP_DB" != "false" ]; then if [ "$SETUP_DB" != "false" ]; then
bundle exec rake db:drop db:create db:schema:load db:migrate setup_db
elif getent hosts postgres || getent hosts mysql; then
if [ "$GITLAB_DATABASE" = "mysql" ]; then setup_db_user_only
bundle exec rake add_limits_mysql
fi
fi fi
...@@ -12,3 +12,21 @@ retry() { ...@@ -12,3 +12,21 @@ retry() {
done done
return 1 return 1
} }
setup_db_user_only() {
if [ "$GITLAB_DATABASE" = "postgresql" ]; then
. scripts/create_postgres_user.sh
else
. scripts/create_mysql_user.sh
fi
}
setup_db() {
setup_db_user_only
bundle exec rake db:drop db:create db:schema:load db:migrate
if [ "$GITLAB_DATABASE" = "mysql" ]; then
bundle exec rake add_limits_mysql
fi
}
...@@ -79,41 +79,18 @@ describe Projects::CommitController do ...@@ -79,41 +79,18 @@ describe Projects::CommitController do
end end
describe "as diff" do describe "as diff" do
include_examples "export as", :diff it "triggers workhorse to serve the request" do
let(:format) { :diff } go(id: commit.id, format: :diff)
it "should really only be a git diff" do expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format)
expect(response.body).to start_with("diff --git")
end
it "is only be a git diff without whitespace changes" do
go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1)
expect(response.body).to start_with("diff --git")
# without whitespace option, there are more than 2 diff_splits for other formats
diff_splits = assigns(:diffs).diff_files.first.diff.diff.split("\n")
expect(diff_splits.length).to be <= 2
end end
end end
describe "as patch" do describe "as patch" do
include_examples "export as", :patch
let(:format) { :patch }
let(:commit2) { project.commit('498214de67004b1da3d820901307bed2a68a8ef6') }
it "is a git email patch" do
go(id: commit2.id, format: format)
expect(response.body).to start_with("From #{commit2.id}")
end
it "contains a git diff" do it "contains a git diff" do
go(id: commit2.id, format: format) go(id: commit.id, format: :patch)
expect(response.body).to match(/^diff --git/) expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:")
end end
end end
......
...@@ -19,11 +19,11 @@ describe Projects::Settings::CiCdController do ...@@ -19,11 +19,11 @@ describe Projects::Settings::CiCdController do
end end
context 'with group runners' do context 'with group runners' do
let(:group_runner) { create(:ci_runner) } let(:group_runner) { create(:ci_runner, runner_type: :group_type) }
let(:parent_group) { create(:group) } let(:parent_group) { create(:group) }
let(:group) { create(:group, runners: [group_runner], parent: parent_group) } let(:group) { create(:group, runners: [group_runner], parent: parent_group) }
let(:other_project) { create(:project, group: group) } let(:other_project) { create(:project, group: group) }
let!(:project_runner) { create(:ci_runner, projects: [other_project]) } let!(:project_runner) { create(:ci_runner, projects: [other_project], runner_type: :project_type) }
let!(:shared_runner) { create(:ci_runner, :shared) } let!(:shared_runner) { create(:ci_runner, :shared) }
it 'sets assignable project runners only' do it 'sets assignable project runners only' do
...@@ -31,7 +31,7 @@ describe Projects::Settings::CiCdController do ...@@ -31,7 +31,7 @@ describe Projects::Settings::CiCdController do
get :show, namespace_id: project.namespace, project_id: project get :show, namespace_id: project.namespace, project_id: project
expect(assigns(:assignable_runners)).to eq [project_runner] expect(assigns(:assignable_runners)).to contain_exactly(project_runner)
end end
end end
end end
......
...@@ -5,18 +5,41 @@ describe 'Invites' do ...@@ -5,18 +5,41 @@ describe 'Invites' do
let(:owner) { create(:user, name: 'John Doe') } let(:owner) { create(:user, name: 'John Doe') }
let(:group) { create(:group, name: 'Owned') } let(:group) { create(:group, name: 'Owned') }
let(:project) { create(:project, :repository, namespace: group) } let(:project) { create(:project, :repository, namespace: group) }
let(:invite) { group.group_members.invite.last } let(:group_invite) { group.group_members.invite.last }
before do before do
project.add_master(owner) project.add_master(owner)
group.add_user(owner, Gitlab::Access::OWNER) group.add_user(owner, Gitlab::Access::OWNER)
group.add_developer('user@example.com', owner) group.add_developer('user@example.com', owner)
invite.generate_invite_token! group_invite.generate_invite_token!
end
def confirm_email_and_sign_in(new_user)
new_user_token = User.find_by_email(new_user.email).confirmation_token
visit user_confirmation_path(confirmation_token: new_user_token)
fill_in_sign_in_form(new_user)
end
def fill_in_sign_up_form(new_user)
fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
fill_in 'new_user_email_confirmation', with: new_user.email
fill_in 'new_user_password', with: new_user.password
click_button "Register"
end
def fill_in_sign_in_form(user)
fill_in 'user_login', with: user.email
fill_in 'user_password', with: user.password
check 'user_remember_me'
click_button 'Sign in'
end end
context 'when signed out' do context 'when signed out' do
before do before do
visit invite_path(invite.raw_invite_token) visit invite_path(group_invite.raw_invite_token)
end end
it 'renders sign in page with sign in notice' do it 'renders sign in page with sign in notice' do
...@@ -25,12 +48,9 @@ describe 'Invites' do ...@@ -25,12 +48,9 @@ describe 'Invites' do
end end
it 'sign in and redirects to invitation page' do it 'sign in and redirects to invitation page' do
fill_in 'user_login', with: user.email fill_in_sign_in_form(user)
fill_in 'user_password', with: user.password
check 'user_remember_me'
click_button 'Sign in'
expect(current_path).to eq(invite_path(invite.raw_invite_token)) expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(page).to have_content( expect(page).to have_content(
'You have been invited by John Doe to join group Owned as Developer.' 'You have been invited by John Doe to join group Owned as Developer.'
) )
...@@ -45,7 +65,7 @@ describe 'Invites' do ...@@ -45,7 +65,7 @@ describe 'Invites' do
end end
it 'shows message user already a member' do it 'shows message user already a member' do
visit invite_path(invite.raw_invite_token) visit invite_path(group_invite.raw_invite_token)
expect(page).to have_content('However, you are already a member of this group.') expect(page).to have_content('However, you are already a member of this group.')
end end
end end
...@@ -53,7 +73,7 @@ describe 'Invites' do ...@@ -53,7 +73,7 @@ describe 'Invites' do
describe 'accepting the invitation' do describe 'accepting the invitation' do
before do before do
sign_in(user) sign_in(user)
visit invite_path(invite.raw_invite_token) visit invite_path(group_invite.raw_invite_token)
end end
it 'grants access and redirects to group page' do it 'grants access and redirects to group page' do
...@@ -69,7 +89,7 @@ describe 'Invites' do ...@@ -69,7 +89,7 @@ describe 'Invites' do
context 'when signed in' do context 'when signed in' do
before do before do
sign_in(user) sign_in(user)
visit invite_path(invite.raw_invite_token) visit invite_path(group_invite.raw_invite_token)
end end
it 'declines application and redirects to dashboard' do it 'declines application and redirects to dashboard' do
...@@ -83,7 +103,7 @@ describe 'Invites' do ...@@ -83,7 +103,7 @@ describe 'Invites' do
context 'when signed out' do context 'when signed out' do
before do before do
visit decline_invite_path(invite.raw_invite_token) visit decline_invite_path(group_invite.raw_invite_token)
end end
it 'declines application and redirects to sign in page' do it 'declines application and redirects to sign in page' do
...@@ -94,4 +114,72 @@ describe 'Invites' do ...@@ -94,4 +114,72 @@ describe 'Invites' do
end end
end end
end end
describe 'invite an user using their email address' do
let(:new_user) { build_stubbed(:user) }
let(:invite_email) { new_user.email }
let(:group_invite) { create(:group_member, :invited, group: group, invite_email: invite_email) }
let!(:project_invite) { create(:project_member, :invited, project: project, invite_email: invite_email) }
before do
stub_application_setting(send_user_confirmation_email: send_email_confirmation)
visit invite_path(group_invite.raw_invite_token)
end
context 'email confirmation disabled' do
let(:send_email_confirmation) { false }
it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do
fill_in_sign_up_form(new_user)
expect(current_path).to eq(dashboard_projects_path)
expect(page).to have_content(project.full_name)
visit group_path(group)
expect(page).to have_content(group.full_name)
end
context 'the user sign-up using a different email address' do
let(:invite_email) { build_stubbed(:user).email }
it 'signs up and redirects to the invitation page' do
fill_in_sign_up_form(new_user)
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
end
end
end
context 'email confirmation enabled' do
let(:send_email_confirmation) { true }
it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
fill_in_sign_up_form(new_user)
confirm_email_and_sign_in(new_user)
expect(current_path).to eq(root_path)
expect(page).to have_content(project.full_name)
visit group_path(group)
expect(page).to have_content(group.full_name)
end
it "doesn't accept invitations until the user confirm his email" do
fill_in_sign_up_form(new_user)
sign_in(owner)
visit project_project_members_path(project)
expect(page).to have_content 'Invited'
end
context 'the user sign-up using a different email address' do
let(:invite_email) { build_stubbed(:user).email }
it 'signs up and redirects to the invitation page' do
fill_in_sign_up_form(new_user)
confirm_email_and_sign_in(new_user)
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
end
end
end
end
end end
require 'spec_helper'
describe Gitlab::Database::Count do
before do
create_list(:project, 3)
end
describe '.execute_estimate_if_updated_recently', :postgresql do
context 'when reltuples have not been updated' do
before do
expect(described_class).to receive(:reltuples_updated_recently?).and_return(false)
end
it 'returns nil' do
expect(described_class.execute_estimate_if_updated_recently(Project)).to be nil
end
end
context 'when reltuples have been updated' do
before do
ActiveRecord::Base.connection.execute('ANALYZE projects')
end
it 'calls postgresql_estimate_query' do
expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original
expect(described_class.execute_estimate_if_updated_recently(Project)).to eq(3)
end
end
end
describe '.approximate_count' do
context 'when reltuples have not been updated' do
it 'counts all projects the normal way' do
allow(described_class).to receive(:reltuples_updated_recently?).and_return(false)
expect(Project).to receive(:count).and_call_original
expect(described_class.approximate_count(Project)).to eq(3)
end
end
context 'no permission' do
it 'falls back to standard query' do
allow(described_class).to receive(:reltuples_updated_recently?).and_raise(PG::InsufficientPrivilege)
expect(Project).to receive(:count).and_call_original
expect(described_class.approximate_count(Project)).to eq(3)
end
end
describe 'when reltuples have been updated', :postgresql do
before do
ActiveRecord::Base.connection.execute('ANALYZE projects')
end
it 'counts all projects in the fast way' do
expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original
expect(described_class.approximate_count(Project)).to eq(3)
end
end
end
end
...@@ -554,24 +554,10 @@ describe Gitlab::Git::Commit, seed_helper: true do ...@@ -554,24 +554,10 @@ describe Gitlab::Git::Commit, seed_helper: true do
it_should_behave_like '#stats' it_should_behave_like '#stats'
end end
describe '#to_diff' do
subject { commit.to_diff }
it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" }
it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
end
describe '#has_zero_stats?' do describe '#has_zero_stats?' do
it { expect(commit.has_zero_stats?).to eq(false) } it { expect(commit.has_zero_stats?).to eq(false) }
end end
describe '#to_patch' do
subject { commit.to_patch }
it { is_expected.to include "From #{SeedRepo::Commit::ID}" }
it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
end
describe '#to_hash' do describe '#to_hash' do
let(:hash) { commit.to_hash } let(:hash) { commit.to_hash }
subject { hash } subject { hash }
......
...@@ -594,7 +594,7 @@ describe Notify do ...@@ -594,7 +594,7 @@ describe Notify do
it 'contains all the useful information' do it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{project.full_name} project" is_expected.to have_subject "Invitation to join the #{project.full_name} project"
is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url is_expected.to have_body_text project.full_name
is_expected.to have_body_text project_member.human_access is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text project_member.invite_token is_expected.to have_body_text project_member.invite_token
end end
......
...@@ -5,7 +5,7 @@ describe Appearance do ...@@ -5,7 +5,7 @@ describe Appearance do
it { is_expected.to be_valid } it { is_expected.to be_valid }
it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_many(:uploads) }
describe '.current', :use_clean_rails_memory_store_caching do describe '.current', :use_clean_rails_memory_store_caching do
let!(:appearance) { create(:appearance) } let!(:appearance) { create(:appearance) }
...@@ -41,4 +41,12 @@ describe Appearance do ...@@ -41,4 +41,12 @@ describe Appearance do
expect(new_row.valid?).to eq(false) expect(new_row.valid?).to eq(false)
end end
end end
context 'with uploads' do
it_behaves_like 'model with mounted uploader', false do
let(:model_object) { create(:appearance, :with_logo) }
let(:upload_attribute) { :logo }
let(:uploader_class) { AttachmentUploader }
end
end
end end
...@@ -626,62 +626,26 @@ describe Ci::Runner do ...@@ -626,62 +626,26 @@ describe Ci::Runner do
end end
describe '.assignable_for' do describe '.assignable_for' do
let(:runner) { create(:ci_runner) } let!(:unlocked_project_runner) { create(:ci_runner, runner_type: :project_type, projects: [project]) }
let!(:locked_project_runner) { create(:ci_runner, runner_type: :project_type, locked: true, projects: [project]) }
let!(:group_runner) { create(:ci_runner, runner_type: :group_type) }
let!(:instance_runner) { create(:ci_runner, :shared) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:another_project) { create(:project) } let(:another_project) { create(:project) }
before do context 'with already assigned project' do
project.runners << runner
end
context 'with shared runners' do
before do
runner.update(is_shared: true)
end
context 'does not give owned runner' do
subject { described_class.assignable_for(project) }
it { is_expected.to be_empty }
end
context 'does not give shared runner' do
subject { described_class.assignable_for(another_project) }
it { is_expected.to be_empty }
end
end
context 'with unlocked runner' do
context 'does not give owned runner' do
subject { described_class.assignable_for(project) } subject { described_class.assignable_for(project) }
it { is_expected.to be_empty } it { is_expected.to be_empty }
end end
context 'does give a specific runner' do context 'with a different project' do
subject { described_class.assignable_for(another_project) } subject { described_class.assignable_for(another_project) }
it { is_expected.to contain_exactly(runner) } it { is_expected.to include(unlocked_project_runner) }
end it { is_expected.not_to include(group_runner) }
end it { is_expected.not_to include(locked_project_runner) }
it { is_expected.not_to include(instance_runner) }
context 'with locked runner' do
before do
runner.update(locked: true)
end
context 'does not give owned runner' do
subject { described_class.assignable_for(project) }
it { is_expected.to be_empty }
end
context 'does not give a locked runner' do
subject { described_class.assignable_for(another_project) }
it { is_expected.to be_empty }
end
end end
end end
......
...@@ -182,7 +182,6 @@ eos ...@@ -182,7 +182,6 @@ eos
it { is_expected.to respond_to(:date) } it { is_expected.to respond_to(:date) }
it { is_expected.to respond_to(:diffs) } it { is_expected.to respond_to(:diffs) }
it { is_expected.to respond_to(:id) } it { is_expected.to respond_to(:id) }
it { is_expected.to respond_to(:to_patch) }
end end
describe '#closes_issues' do describe '#closes_issues' do
......
...@@ -15,7 +15,7 @@ describe Group do ...@@ -15,7 +15,7 @@ describe Group do
it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:labels).class_name('GroupLabel') } it { is_expected.to have_many(:labels).class_name('GroupLabel') }
it { is_expected.to have_many(:variables).class_name('Ci::GroupVariable') } it { is_expected.to have_many(:variables).class_name('Ci::GroupVariable') }
it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_many(:uploads) }
it { is_expected.to have_one(:chat_team) } it { is_expected.to have_one(:chat_team) }
it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') } it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') }
it { is_expected.to have_many(:badges).class_name('GroupBadge') } it { is_expected.to have_many(:badges).class_name('GroupBadge') }
...@@ -691,4 +691,12 @@ describe Group do ...@@ -691,4 +691,12 @@ describe Group do
end end
end end
end end
context 'with uploads' do
it_behaves_like 'model with mounted uploader', true do
let(:model_object) { create(:group, :with_avatar) }
let(:upload_attribute) { :avatar }
let(:uploader_class) { AttachmentUploader }
end
end
end end
...@@ -76,7 +76,7 @@ describe Project do ...@@ -76,7 +76,7 @@ describe Project do
it { is_expected.to have_many(:project_group_links) } it { is_expected.to have_many(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:delete_all) } it { is_expected.to have_many(:notification_settings).dependent(:delete_all) }
it { is_expected.to have_many(:forks).through(:forked_project_links) } it { is_expected.to have_many(:forks).through(:forked_project_links) }
it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_many(:uploads) }
it { is_expected.to have_many(:pipeline_schedules) } it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:clusters) } it { is_expected.to have_many(:clusters) }
...@@ -3739,4 +3739,12 @@ describe Project do ...@@ -3739,4 +3739,12 @@ describe Project do
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
end end
context 'with uploads' do
it_behaves_like 'model with mounted uploader', true do
let(:model_object) { create(:project, :with_avatar) }
let(:upload_attribute) { :avatar }
let(:uploader_class) { AttachmentUploader }
end
end
end end
...@@ -39,7 +39,7 @@ describe User do ...@@ -39,7 +39,7 @@ describe User do
it { is_expected.to have_many(:builds).dependent(:nullify) } it { is_expected.to have_many(:builds).dependent(:nullify) }
it { is_expected.to have_many(:pipelines).dependent(:nullify) } it { is_expected.to have_many(:pipelines).dependent(:nullify) }
it { is_expected.to have_many(:chat_names).dependent(:destroy) } it { is_expected.to have_many(:chat_names).dependent(:destroy) }
it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_many(:uploads) }
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') } it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') } it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') }
...@@ -1223,6 +1223,24 @@ describe User do ...@@ -1223,6 +1223,24 @@ describe User do
end end
end end
describe '#accept_pending_invitations!' do
let(:user) { create(:user, email: 'user@email.com') }
let!(:project_member_invite) { create(:project_member, :invited, invite_email: user.email) }
let!(:group_member_invite) { create(:group_member, :invited, invite_email: user.email) }
let!(:external_project_member_invite) { create(:project_member, :invited, invite_email: 'external@email.com') }
let!(:external_group_member_invite) { create(:group_member, :invited, invite_email: 'external@email.com') }
it 'accepts all the user members pending invitations and returns the accepted_members' do
accepted_members = user.accept_pending_invitations!
expect(accepted_members).to match_array([project_member_invite, group_member_invite])
expect(group_member_invite.reload).not_to be_invite
expect(project_member_invite.reload).not_to be_invite
expect(external_project_member_invite.reload).to be_invite
expect(external_group_member_invite.reload).to be_invite
end
end
describe '#all_emails' do describe '#all_emails' do
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -1786,28 +1804,54 @@ describe User do ...@@ -1786,28 +1804,54 @@ describe User do
end end
end end
describe '#ci_authorized_runners' do describe '#ci_owned_runners' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:runner) { create(:ci_runner) } let(:runner_1) { create(:ci_runner) }
let(:runner_2) { create(:ci_runner) }
before do context 'without any projects nor groups' do
project.runners << runner let!(:project) { create(:project, runners: [runner_1]) }
end let!(:group) { create(:group) }
context 'without any projects' do
let(:project) { create(:project) }
it 'does not load' do it 'does not load' do
expect(user.ci_authorized_runners).to be_empty expect(user.ci_owned_runners).to be_empty
end end
end end
context 'with personal projects runners' do context 'with personal projects runners' do
let(:namespace) { create(:namespace, owner: user) } let(:namespace) { create(:namespace, owner: user) }
let(:project) { create(:project, namespace: namespace) } let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) }
it 'loads' do
expect(user.ci_owned_runners).to contain_exactly(runner_1)
end
end
context 'with personal group runner' do
let!(:project) { create(:project, runners: [runner_1]) }
let!(:group) do
create(:group, runners: [runner_2]).tap do |group|
group.add_owner(user)
end
end
it 'loads' do
expect(user.ci_owned_runners).to contain_exactly(runner_2)
end
end
context 'with personal project and group runner' do
let(:namespace) { create(:namespace, owner: user) }
let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) }
let!(:group) do
create(:group, runners: [runner_2]).tap do |group|
group.add_owner(user)
end
end
it 'loads' do it 'loads' do
expect(user.ci_authorized_runners).to contain_exactly(runner) expect(user.ci_owned_runners).to contain_exactly(runner_1, runner_2)
end end
end end
...@@ -1818,7 +1862,7 @@ describe User do ...@@ -1818,7 +1862,7 @@ describe User do
end end
it 'loads' do it 'loads' do
expect(user.ci_authorized_runners).to contain_exactly(runner) expect(user.ci_owned_runners).to contain_exactly(runner_1)
end end
end end
...@@ -1828,14 +1872,28 @@ describe User do ...@@ -1828,14 +1872,28 @@ describe User do
end end
it 'does not load' do it 'does not load' do
expect(user.ci_authorized_runners).to be_empty expect(user.ci_owned_runners).to be_empty
end end
end end
end end
context 'with groups projects runners' do context 'with groups projects runners' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:project) { create(:project, group: group) } let!(:project) { create(:project, group: group, runners: [runner_1]) }
def add_user(access)
group.add_user(user, access)
end
it_behaves_like :member
end
context 'with groups runners' do
let!(:group) do
create(:group, runners: [runner_1]).tap do |group|
group.add_owner(user)
end
end
def add_user(access) def add_user(access)
group.add_user(user, access) group.add_user(user, access)
...@@ -1845,7 +1903,7 @@ describe User do ...@@ -1845,7 +1903,7 @@ describe User do
end end
context 'with other projects runners' do context 'with other projects runners' do
let(:project) { create(:project) } let!(:project) { create(:project, runners: [runner_1]) }
def add_user(access) def add_user(access)
project.add_role(user, access) project.add_role(user, access)
...@@ -1858,7 +1916,7 @@ describe User do ...@@ -1858,7 +1916,7 @@ describe User do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:another_user) { create(:user) } let(:another_user) { create(:user) }
let(:subgroup) { create(:group, parent: group) } let(:subgroup) { create(:group, parent: group) }
let(:project) { create(:project, group: subgroup) } let!(:project) { create(:project, group: subgroup, runners: [runner_1]) }
def add_user(access) def add_user(access)
group.add_user(user, access) group.add_user(user, access)
...@@ -2769,4 +2827,12 @@ describe User do ...@@ -2769,4 +2827,12 @@ describe User do
expect { user.increment_failed_attempts! }.not_to change(user, :failed_attempts) expect { user.increment_failed_attempts! }.not_to change(user, :failed_attempts)
end end
end end
context 'with uploads' do
it_behaves_like 'model with mounted uploader', false do
let(:model_object) { create(:user, :with_avatar) }
let(:upload_attribute) { :avatar }
let(:uploader_class) { AttachmentUploader }
end
end
end end
...@@ -27,7 +27,7 @@ describe API::Runners do ...@@ -27,7 +27,7 @@ describe API::Runners do
end end
end end
let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group]) } let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group], runner_type: :group_type) }
before do before do
# Set project access for users # Set project access for users
...@@ -48,7 +48,7 @@ describe API::Runners do ...@@ -48,7 +48,7 @@ describe API::Runners do
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response[0]).to have_key('ip_address') expect(json_response[0]).to have_key('ip_address')
expect(descriptions).to contain_exactly( expect(descriptions).to contain_exactly(
'Project runner', 'Two projects runner' 'Project runner', 'Two projects runner', 'Group runner'
) )
expect(shared).to be_falsey expect(shared).to be_falsey
end end
...@@ -592,6 +592,15 @@ describe API::Runners do ...@@ -592,6 +592,15 @@ describe API::Runners do
end.to change { project.runners.count }.by(+1) end.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(201)
end end
it 'enables a shared runner' do
expect do
post api("/projects/#{project.id}/runners", admin), runner_id: shared_runner.id
end.to change { project.runners.count }.by(1)
expect(shared_runner.reload).not_to be_shared
expect(response).to have_gitlab_http_status(201)
end
end end
context 'user is not admin' do context 'user is not admin' do
......
require 'spec_helper'
shared_examples_for 'model with mounted uploader' do |supports_fileuploads|
describe '.destroy' do
before do
stub_uploads_object_storage(uploader_class)
model_object.public_send(upload_attribute).migrate!(ObjectStorage::Store::REMOTE)
end
it 'deletes remote uploads' do
expect_any_instance_of(CarrierWave::Storage::Fog::File).to receive(:delete).and_call_original
expect { model_object.destroy }.to change { Upload.count }.by(-1)
end
it 'deletes any FileUploader uploads which are not mounted', skip: !supports_fileuploads do
create(:upload, uploader: FileUploader, model: model_object)
expect { model_object.destroy }.to change { Upload.count }.by(-2)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment