Commit 49673de3 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 46381-dropdown-mr-widget

* master: (40 commits)
  Add changelog
  Update quick_start_guide.md
  Resolve "Opening Project with invite but without accepting leads to 404 error page"
  Respect the inheritance chain between Ci::Build and CommitStatus
  Remove unneccessary imports
  fixed copy to cliboard button in embedded snippets
  Fix Error 500 viewing admin page due to statement timeouts
  Grant privileges after database is created
  Only setup db in the first checkout!
  Project Sidebar: Split CI/CD into CI/CD and Operations
  Fix GPM content types for Doorkeeper
  Workhorse to send raw diff and patch for commits
  Refactor out duplication in runner_policy.rb
  Remove unnecessary runner.is_shared? checks in api because they are handled by policy
  Allow admin to assign shared runner to project through API
  Change policy list_runner_jobs -> read_runner
  Rename User#ci_authorized_runners -> ci_owned_runners
  Improve efficiency of authorized_runner policy query
  Use can? policies for lib/api/runners.rb
  Allow group runners to be viewed/edited in API
  ...
parents 9e61d26c d9b78477
......@@ -189,7 +189,7 @@ stages:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
variables:
CREATE_DB_USER: "true"
SETUP_DB: "false"
script:
# Manually clone gitlab-test and only seed this project in
# db/fixtures/development/04_project.rb thanks to SIZE=1 below
......@@ -233,7 +233,7 @@ stages:
.migration-paths: &migration-paths
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables:
CREATE_DB_USER: "true"
SETUP_DB: "false"
script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
......@@ -242,7 +242,7 @@ stages:
- cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- date
- git checkout $CI_COMMIT_SHA
- git checkout -f $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS
- date
- . scripts/prepare_build.sh
......
......@@ -5,8 +5,3 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
---
All Documentation content that resides under the doc/ directory of this
repository is licensed under Creative Commons: CC BY-SA 4.0.
......@@ -73,6 +73,8 @@ GitLab Community Edition (CE) is available freely under the MIT Expat license.
All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0.
## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
......
......@@ -179,7 +179,7 @@
role="row"
>
<div
class="alert alert-danger alert-block append-bottom-0 table-section section-100"
class="alert alert-danger alert-block append-bottom-0"
role="gridcell"
>
<div>
......
......@@ -32,17 +32,17 @@ export default {
<template>
<div :class="className">
{{ actionText }}
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom"
/>
<template v-if="editedBy">
by
{{ s__('ByAuthor|by') }}
<a
:href="editedBy.path"
class="js-vue-author author_link">
{{ editedBy.name }}
</a>
</template>
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom"
/>
</div>
</template>
......@@ -62,6 +62,21 @@ export default {
<template>
<div class="note-header-info">
<div
v-if="includeToggle"
class="discussion-actions">
<button
@click="handleToggle"
class="note-action-button discussion-toggle-button js-vue-toggle-button"
type="button">
<i
:class="toggleChevronClass"
class="fa"
aria-hidden="true">
</i>
{{ __('Toggle discussion') }}
</button>
</div>
<a :href="author.path">
<span class="note-header-author-name">{{ author.name }}</span>
<span class="note-headline-light">
......@@ -78,10 +93,13 @@ export default {
v-html="actionTextHtml"
class="system-note-message">
</span>
<span class="system-note-separator">
&middot;
</span>
<a
:href="noteTimestampLink"
@click="updateTargetNoteHash"
class="note-timestamp">
class="note-timestamp system-note-separator">
<time-ago-tooltip
:time="createdAt"
tooltip-placement="bottom"
......@@ -95,20 +113,5 @@ export default {
</i>
</span>
</span>
<div
v-if="includeToggle"
class="discussion-actions">
<button
@click="handleToggle"
class="note-action-button discussion-toggle-button js-vue-toggle-button"
type="button">
<i
:class="toggleChevronClass"
class="fa"
aria-hidden="true">
</i>
Toggle discussion
</button>
</div>
</div>
</template>
/* 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 'underscore';
......@@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats';
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 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() {
function ContributorsGraph() {}
ContributorsGraph.prototype.MARGIN = {
top: 20,
right: 20,
right: 10,
bottom: 30,
left: 50
left: 40
};
ContributorsGraph.prototype.x_domain = null;
......@@ -32,6 +32,12 @@ export const ContributorsGraph = (function() {
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) {
return ContributorsGraph.prototype.x_domain = data;
};
......@@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) {
function ContributorsMasterGraph(data1) {
const $parentElement = $('#contributors-master');
const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
this.data = data1;
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.x = null;
this.y = null;
......@@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) {
}
ContributorsMasterGraph.prototype.process_dates = function(data) {
var dates;
dates = this.get_dates(data);
const dates = this.get_dates(data);
this.parse_dates(data);
return ContributorsGraph.set_dates(dates);
};
......@@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.parse_dates = function(data) {
var parseDate;
parseDate = d3.timeParse("%Y-%m-%d");
const parseDate = d3.timeParse("%Y-%m-%d");
return data.forEach(function(d) {
return d.date = parseDate(d.date);
});
......@@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) {
};
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) {
......@@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) {
extend(ContributorsAuthorGraph, superClass);
function ContributorsAuthorGraph(data1) {
const $parentElements = $('.person');
this.data = data1;
// Don't split graph size in half for mobile devices.
if ($(window).width() < 768) {
this.width = $('.content').width() - 80;
if ($(window).width() < 790) {
this.width = this.determine_width($('.js-graphs-show').width(), $parentElements);
} else {
this.width = ($('.content').width() / 2) - 100;
this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements);
}
this.height = 200;
this.x = null;
......@@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) {
ContributorsAuthorGraph.prototype.create_area = function(x, y) {
return this.area = d3.area().x(function(d) {
var parseDate;
parseDate = d3.timeParse("%Y-%m-%d");
const parseDate = d3.timeParse("%Y-%m-%d");
return x(parseDate(d));
}).y0(this.height).y1((function(_this) {
return function(d) {
......@@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) {
};
ContributorsAuthorGraph.prototype.create_svg = function() {
var persons = document.querySelectorAll('.person');
const persons = document.querySelectorAll('.person');
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) {
......
......@@ -40,7 +40,7 @@ export default {
:class="cssClass"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
data-container="body">
{{ timeFormated(time) }}
data-container="body"
v-text="timeFormated(time)">
</time>
</template>
......@@ -67,7 +67,8 @@
padding: 8px 40px;
}
.embed-toggle {
.embed-toggle,
.snippet-clipboard-btn {
height: 35px;
}
}
@import './issues/issue_count_badge';
[v-cloak] {
display: none;
}
......
@import "./issues/issue_count_badge";
.issues-list {
.issue {
padding: 10px 0 10px $gl-padding;
......
......@@ -407,10 +407,6 @@ ul.notes {
.note-header {
display: flex;
justify-content: space-between;
@include notes-media('max', $screen-xs-max) {
flex-flow: row wrap;
}
}
.note-header-info {
......@@ -459,6 +455,10 @@ ul.notes {
white-space: normal;
}
.system-note-separator {
color: $gl-text-color-disabled;
}
a:hover {
text-decoration: underline;
}
......@@ -473,11 +473,6 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
@include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
}
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
......
class Admin::DashboardController < Admin::ApplicationController
include CountHelper
def index
@projects = Project.order_id_desc.without_deleted.with_route.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
include AcceptsPendingInvitations
def almost_there
flash[:notice] = nil
render layout: "devise_empty"
......@@ -11,6 +13,8 @@ class ConfirmationsController < Devise::ConfirmationsController
end
def after_confirmation_path_for(resource_name, resource)
accept_pending_invitations
# incoming resource can either be a :user or an :email
if signed_in?(:user)
after_sign_in(resource)
......
......@@ -23,8 +23,12 @@ class Projects::CommitController < Projects::ApplicationController
respond_to do |format|
format.html { render }
format.diff { render text: @commit.to_diff }
format.patch { render text: @commit.to_patch }
format.diff do
send_git_diff(@project.repository, @commit.diff_refs)
end
format.patch do
send_git_patch(@project.repository, @commit.diff_refs)
end
end
end
......
......@@ -69,7 +69,7 @@ module Projects
@project_runners = @project.runners.ordered
@assignable_runners = current_user
.ci_authorized_runners
.ci_owned_runners
.assignable_for(project)
.ordered
.page(params[:page]).per(20)
......
class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
include AcceptsPendingInvitations
before_action :whitelist_query_limiting, only: [:destroy]
......@@ -16,6 +17,7 @@ class RegistrationsController < Devise::RegistrationsController
end
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
accept_pending_invitations
super
else
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
......@@ -60,7 +62,7 @@ class RegistrationsController < Devise::RegistrationsController
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?}")
user.confirmed? ? dashboard_projects_path : users_almost_there_path
user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path
end
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
......@@ -257,6 +257,7 @@ module ProjectsHelper
if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
nav_tabs << :operations
end
if project.external_issue_tracker
......
......@@ -2,6 +2,7 @@ class Appearance < ActiveRecord::Base
include CacheMarkdownField
include AfterCommitQueue
include ObjectStorage::BackgroundMove
include WithUploads
cache_markdown_field :description
cache_markdown_field :new_project_guidelines
......@@ -14,8 +15,6 @@ class Appearance < ActiveRecord::Base
mount_uploader :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
after_commit :flush_redis_cache
......
......@@ -52,7 +52,7 @@ module Ci
# Without that, placeholders would miss one and couldn't match.
where(locked: false)
.where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})")
.specific
.project_type
end
validate :tag_constraints
......
......@@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base
include HasStatus
include Importable
include AfterCommitQueue
include Presentable
self.table_name = 'ci_builds'
......
# 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
include LoadedInGroupList
include GroupDescendant
include TokenAuthenticatable
include WithUploads
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
......@@ -30,8 +31,6 @@ class Group < Namespace
has_many :variables, class_name: 'Ci::GroupVariable'
has_many :custom_attributes, class_name: 'GroupCustomAttribute'
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :boards
has_many :badges, class_name: 'GroupBadge'
......
......@@ -31,7 +31,8 @@ class List < ActiveRecord::Base
if options.key?(:label)
json[:label] = label.as_json(
project: board.project,
only: [:id, :title, :description, :color]
only: [:id, :title, :description, :color],
methods: [:text_color]
)
end
end
......
......@@ -23,6 +23,7 @@ class Project < ActiveRecord::Base
include ::Gitlab::Utils::StrongMemoize
include ChronicDurationAttribute
include FastDestroyAll::Helpers
include WithUploads
extend Gitlab::ConfigHelper
......@@ -301,8 +302,6 @@ class Project < ActiveRecord::Base
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
validates :variables, variable_duplicates: { scope: :environment_scope }
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Scopes
scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) }
......
......@@ -17,6 +17,7 @@ class User < ActiveRecord::Base
include IgnorableColumn
include BulkMemberAccessLoad
include BlocksJsonSerialization
include WithUploads
DEFAULT_NOTIFICATION_LEVEL = :participating
......@@ -137,7 +138,6 @@ class User < ActiveRecord::Base
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
......@@ -860,6 +860,16 @@ class User < ActiveRecord::Base
confirmed? && !temp_oauth_email?
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
all_emails = []
all_emails << email unless temp_oauth_email?
......@@ -999,12 +1009,19 @@ class User < ActiveRecord::Base
!solo_owned_groups.present?
end
def ci_authorized_runners
@ci_authorized_runners ||= begin
runner_ids = Ci::RunnerProject
def ci_owned_runners
@ci_owned_runners ||= begin
project_runner_ids = Ci::RunnerProject
.where(project: authorized_projects(Gitlab::Access::MASTER))
.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
......@@ -1205,6 +1222,11 @@ class User < ActiveRecord::Base
!terms_accepted?
end
def owned_or_masters_groups
union = Gitlab::SQL::Union.new([owned_groups, masters_groups])
Group.from("(#{union.to_sql}) namespaces")
end
protected
# override, from Devise::Validatable
......
module Ci
class RunnerPolicy < BasePolicy
with_options scope: :subject, score: 0
condition(:shared) { @subject.is_shared? }
with_options scope: :subject, score: 0
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 { 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
end
end
module Ci
class BuildPresenter < Gitlab::View::Presenter::Delegated
CALLOUT_FAILURE_MESSAGES = {
unknown_failure: 'There is an unknown failure, please try again',
script_failure: 'There has been a script failure. Check the job log for more information',
api_failure: 'There has been an API failure, please try again',
stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
runner_system_failure: 'There has been a runner system failure, please try again',
missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
}.freeze
presents :build
class BuildPresenter < CommitStatusPresenter
def erased_by_user?
# Build can be erased through API, therefore it does not have
# `erased_by` user assigned in that case.
......@@ -44,14 +33,6 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}"
end
def callout_failure_message
CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
end
def recoverable?
failed? && !unrecoverable?
end
private
def tooltip_for_badge
......@@ -61,9 +42,5 @@ module Ci
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
def unrecoverable?
script_failure? || missing_dependency_failure?
end
end
end
class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
CALLOUT_FAILURE_MESSAGES = {
unknown_failure: 'There is an unknown failure, please try again',
script_failure: 'There has been a script failure. Check the job log for more information',
api_failure: 'There has been an API failure, please try again',
stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
runner_system_failure: 'There has been a runner system failure, please try again',
missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
}.freeze
presents :build
def callout_failure_message
CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
end
def recoverable?
failed? && !unrecoverable?
end
def unrecoverable?
script_failure? || missing_dependency_failure?
end
end
class GenericCommitStatusPresenter < CommitStatusPresenter
end
......@@ -10,7 +10,7 @@
= link_to admin_projects_path do
%h3.text-center
Projects:
= number_with_delimiter(Project.cached_count)
= approximate_count_with_delimiters(Project)
%hr
= link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4
......@@ -19,7 +19,7 @@
= link_to admin_users_path do
%h3.text-center
Users:
= number_with_delimiter(User.count)
= approximate_count_with_delimiters(User)
%hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new"
.col-sm-4
......@@ -28,7 +28,7 @@
= link_to admin_groups_path do
%h3.text-center
Groups:
= number_with_delimiter(Group.count)
= approximate_count_with_delimiters(Group)
%hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row
......@@ -39,31 +39,31 @@
%p
Forks
%span.light.pull-right
= number_with_delimiter(ForkedProjectLink.count)
= approximate_count_with_delimiters(ForkedProjectLink)
%p
Issues
%span.light.pull-right
= number_with_delimiter(Issue.count)
= approximate_count_with_delimiters(Issue)
%p
Merge Requests
%span.light.pull-right
= number_with_delimiter(MergeRequest.count)
= approximate_count_with_delimiters(MergeRequest)
%p
Notes
%span.light.pull-right
= number_with_delimiter(Note.count)
= approximate_count_with_delimiters(Note)
%p
Snippets
%span.light.pull-right
= number_with_delimiter(Snippet.count)
= approximate_count_with_delimiters(Snippet)
%p
SSH Keys
%span.light.pull-right
= number_with_delimiter(Key.count)
= approximate_count_with_delimiters(Key)
%p
Milestones
%span.light.pull-right
= number_with_delimiter(Milestone.count)
= approximate_count_with_delimiters(Milestone)
%p
Active Users
%span.light.pull-right
......
......@@ -13,7 +13,7 @@
= icon("chevron-up")
- else
= icon("chevron-down")
Toggle discussion
= _('Toggle discussion')
= link_to_member(@project, discussion.author, avatar: false)
.inline.discussion-headline-light
......
......@@ -13,13 +13,13 @@
.nav-icon-container
= sprite_icon('project')
%span.nav-item-name
Project
= _('Project')
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
= link_to project_path(@project) do
%strong.fly-out-top-item-name
#{ _('Overview') }
= _('Overview')
%li.divider.fly-out-top-item
= nav_link(path: 'projects#show') do
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
......@@ -40,45 +40,45 @@
.nav-icon-container
= sprite_icon('doc_text')
%span.nav-item-name
Repository
= _('Repository')
%ul.sidebar-sub-level-items
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network), html_options: { class: "fly-out-top-item" } ) do
= link_to project_tree_path(@project) do
%strong.fly-out-top-item-name
#{ _('Repository') }
= _('Repository')
%li.divider.fly-out-top-item
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_tree_path(@project) do
#{ _('Files') }
= _('Files')
= nav_link(controller: [:commit, :commits]) do
= link_to project_commits_path(@project, current_ref) do
#{ _('Commits') }
= _('Commits')
= nav_link(html_options: {class: branches_tab_class}) do
= link_to project_branches_path(@project) do
#{ _('Branches') }
= _('Branches')
= nav_link(controller: [:tags, :releases]) do
= link_to project_tags_path(@project) do
#{ _('Tags') }
= _('Tags')
= nav_link(path: 'graphs#show') do
= link_to project_graph_path(@project, current_ref) do
#{ _('Contributors') }
= _('Contributors')
= nav_link(controller: %w(network)) do
= link_to project_network_path(@project, current_ref) do
#{ s_('ProjectNetworkGraph|Graph') }
= _('Graph')
= nav_link(controller: :compare) do
= link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
#{ _('Compare') }
= _('Compare')
= nav_link(path: 'graphs#charts') do
= link_to charts_project_graph_path(@project, current_ref) do
#{ _('Charts') }
= _('Charts')
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
......@@ -86,7 +86,7 @@
.nav-icon-container
= sprite_icon('issues')
%span.nav-item-name
Issues
= _('Issues')
- if @project.issues_enabled?
%span.badge.count.issue_counter
= number_with_delimiter(@project.open_issues_count)
......@@ -95,7 +95,7 @@
= nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do
= link_to project_issues_path(@project) do
%strong.fly-out-top-item-name
#{ _('Issues') }
= _('Issues')
- if @project.issues_enabled?
%span.badge.count.issue_counter.fly-out-badge
= number_with_delimiter(@project.open_issues_count)
......@@ -103,7 +103,7 @@
= nav_link(controller: :issues, action: :index) do
= link_to project_issues_path(@project), title: 'Issues' do
%span
List
= _('List')
= nav_link(controller: :boards) do
= link_to project_boards_path(@project), title: boards_link_text do
......@@ -113,12 +113,12 @@
= nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: 'Labels' do
%span
Labels
= _('Labels')
= nav_link(controller: :milestones) do
= link_to project_milestones_path(@project), title: 'Milestones' do
%span
Milestones
= _('Milestones')
- if project_nav_tab? :external_issue_tracker
= nav_link do
- issue_tracker = @project.external_issue_tracker
......@@ -139,54 +139,75 @@
.nav-icon-container
= sprite_icon('git-merge')
%span.nav-item-name
Merge Requests
= _('Merge Requests')
%span.badge.count.merge_counter.js-merge-counter
= number_with_delimiter(@project.open_merge_requests_count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
= link_to project_merge_requests_path(@project) do
%strong.fly-out-top-item-name
#{ _('Merge Requests') }
= _('Merge Requests')
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge
= number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp]) do
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts]) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container
= sprite_icon('pipeline')
%span.nav-item-name
CI / CD
= _('CI / CD')
%ul.sidebar-sub-level-items
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], html_options: { class: "fly-out-top-item" } ) do
= link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name
#{ _('CI / CD') }
= _('CI / CD')
%li.divider.fly-out-top-item
- if project_nav_tab? :pipelines
= nav_link(path: ['pipelines#index', 'pipelines#show']) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
= _('Pipelines')
- if project_nav_tab? :builds
= nav_link(controller: [:jobs, :artifacts]) do
= link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
Jobs
= _('Jobs')
- if project_nav_tab? :pipelines
= nav_link(controller: :pipeline_schedules) do
= link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do
%span
Schedules
= _('Schedules')
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do
= link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
%span
= _('Charts')
- if project_nav_tab? :operations
= nav_link(controller: [:environments, :clusters, :user, :gcp]) do
= link_to project_environments_path(@project), class: 'shortcuts-operations' do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
= _('Operations')
%ul.sidebar-sub-level-items
= nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
= link_to project_environments_path(@project) do
%strong.fly-out-top-item-name
= _('Operations')
%li.divider.fly-out-top-item
- if project_nav_tab? :environments
= nav_link(controller: :environments) do
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span
Environments
= _('Environments')
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
......@@ -217,19 +238,18 @@
%span= _("Got it!")
= sprite_icon('thumb-up')
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do
= link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
%span
Charts
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
.nav-icon-container
= sprite_icon('disk')
%span.nav-item-name
Registry
= _('Registry')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: %w[projects/registry/repositories], html_options: { class: "fly-out-top-item" } ) do
= link_to project_container_registry_index_path(@project) do
%strong.fly-out-top-item-name
= _('Registry')
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
......@@ -237,12 +257,12 @@
.nav-icon-container
= sprite_icon('book')
%span.nav-item-name
Wiki
= _('Wiki')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
= link_to get_project_wiki_path(@project) do
%strong.fly-out-top-item-name
#{ _('Wiki') }
= _('Wiki')
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
......@@ -250,12 +270,12 @@
.nav-icon-container
= sprite_icon('snippet')
%span.nav-item-name
Snippets
= _('Snippets')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
= link_to project_snippets_path(@project) do
%strong.fly-out-top-item-name
#{ _('Snippets') }
= _('Snippets')
- if project_nav_tab? :settings
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show]) do
......@@ -263,7 +283,7 @@
.nav-icon-container
= sprite_icon('settings')
%span.nav-item-name.qa-settings-item
Settings
= _('Settings')
%ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project)
......@@ -271,16 +291,16 @@
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show], html_options: { class: "fly-out-top-item" } ) do
= link_to edit_project_path(@project) do
%strong.fly-out-top-item-name
#{ _('Settings') }
= _('Settings')
%li.divider.fly-out-top-item
= nav_link(path: %w[projects#edit]) do
= link_to edit_project_path(@project), title: 'General' do
%span
General
= _('General')
= nav_link(controller: :project_members) do
= link_to project_project_members_path(@project), title: 'Members' do
%span
Members
= _('Members')
- if can_edit
= nav_link(controller: :badges) do
= link_to project_settings_badges_path(@project), title: _('Badges') do
......@@ -290,21 +310,21 @@
= nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span
Integrations
= _('Integrations')
= nav_link(controller: :repository) do
= link_to project_settings_repository_path(@project), title: 'Repository' do
%span
Repository
= _('Repository')
- if @project.feature_available?(:builds, current_user)
= nav_link(controller: :ci_cd) do
= link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do
%span
CI / CD
= _('CI / CD')
- if @project.pages_available?
= nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: 'Pages' do
%span
Pages
= _('Pages')
- else
= nav_link(controller: :project_members) do
......@@ -312,12 +332,12 @@
.nav-icon-container
= sprite_icon('users')
%span.nav-item-name
Members
= _('Members')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
= link_to project_project_members_path(@project) do
%strong.fly-out-top-item-name
#{ _('Members') }
= _('Members')
= render 'shared/sidebar_toggle_button'
......
......@@ -4,7 +4,7 @@
by
= link_to member.created_by.name, user_url(member.created_by)
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}.
%p
......
......@@ -15,7 +15,7 @@
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
class: "label color-label title board-title-text",
":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.text_color ? list.label.text_color : \"#2e2e2e\") }" }
":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" }
{{ list.title }}
- if can?(current_user, :admin_list, current_board_parent)
......
......@@ -41,8 +41,9 @@
- if note.system
%span.system-note-message
= markdown_field(note, :note)
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
%span.system-note-separator
&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?
.note-actions
- if note.for_personal_snippet?
......
......@@ -45,6 +45,6 @@
%strong.embed-toggle-list-item= _("Share")
%input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
.input-group-btn
%button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' }
%button.js-clipboard-btn.snippet-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '.js-snippet-url-area' }
= sprite_icon('duplicate', size: 16)
.clearfix
---
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: Move project sidebar sub-entries 'Environments' and 'Kubernetes' from 'CI/CD' to a new entry 'Operations'
merge_request: 18941
author:
type: changed
---
title: Allow CommitStatus class to use presentable methods
merge_request: 18979
author:
type: fixed
---
title: fixed copy to blipboard button in embed bar of snippets
merge_request: 18923
author: haseebeqx
type: fixed
---
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: Move discussion actions to the right for small viewports
merge_request: 18476
author: George Tsiolis
type: changed
---
title: Adding branches through the WebUI is handled by Gitaly
merge_request:
author:
type: other
---
title: Workhorse to send raw diff and patch for commits
merge_request:
author:
type: other
......@@ -23,6 +23,10 @@ page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the
**Fork** button. Soon you should have a project under your namespace with the
necessary files.
You can also start a new project from a
[GitLab project template](https://gitlab.com/gitlab-org/project-templates) if
you want to use a different language.
## Setup your own cluster on Google Kubernetes Engine
If you do not already have a Google Cloud account, create one at
......
......@@ -165,6 +165,7 @@ module API
group = find_group!(params[:id])
authorize! :admin_group, group
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
destroy_conditionally!(group) do |group|
::Groups::DestroyService.new(group, current_user).execute
end
......
......@@ -14,7 +14,7 @@ module API
use :pagination
end
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
end
......@@ -184,40 +184,35 @@ module API
def authenticate_show_runner!(runner)
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
def authenticate_update_runner!(runner)
return if current_user.admin?
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("No access granted") unless user_can_access_runner?(runner)
forbidden!("No access granted") unless can?(current_user, :update_runner, runner)
end
def authenticate_delete_runner!(runner)
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!("No access granted") unless user_can_access_runner?(runner)
forbidden!("No access granted") unless can?(current_user, :delete_runner, runner)
end
def authenticate_enable_runner!(runner)
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner is locked") if runner.locked?
forbidden!("Runner is a group runner") if runner.group_type?
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
def authenticate_list_runners_jobs!(runner)
return if current_user.admin?
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
def user_can_access_runner?(runner)
current_user.ci_authorized_runners.exists?(runner.id)
forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
end
end
end
......
......@@ -131,6 +131,7 @@ module API
delete ":id" do
group = find_group!(params[:id])
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
end
......
......@@ -58,7 +58,7 @@ module API
end
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
......
# 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
parent_ids.first
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.
# 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+
......@@ -432,16 +417,6 @@ module Gitlab
Gitlab::Git::CommitStats.new(@repository, self)
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
#
# Ex.
......
......@@ -776,13 +776,9 @@ module Gitlab
end
def add_branch(branch_name, user:, target:)
gitaly_migrate(:operation_user_create_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_add_branch(branch_name, user, target)
else
rugged_add_branch(branch_name, user, target)
end
end
gitaly_operation_client.user_create_branch(branch_name, user, target)
rescue GRPC::FailedPrecondition => ex
raise InvalidRef, ex
end
def add_tag(tag_name, user:, target:, message: nil)
......@@ -2197,22 +2193,6 @@ module Gitlab
end
end
def gitaly_add_branch(branch_name, user, target)
gitaly_operation_client.user_create_branch(branch_name, user, target)
rescue GRPC::FailedPrecondition => ex
raise InvalidRef, ex
end
def rugged_add_branch(branch_name, user, target)
target_object = Ref.dereference_object(lookup(target))
raise InvalidRef.new("target not found: #{target}") unless target_object
OperationService.new(user, self).add_branch(branch_name, target_object.oid)
find_branch(branch_name)
rescue Rugged::ReferenceError => ex
raise InvalidRef, ex
end
def rugged_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch(
branch_name,
......
......@@ -28,7 +28,11 @@ module Gitlab
controller = @env[CONTROLLER_KEY]
action = "#{controller.action_name}"
suffix = controller.request_format
# Devise exposes a method called "request_format" that does the below.
# However, this method is not available to all controllers (e.g. certain
# Doorkeeper controllers). As such we use the underlying code directly.
suffix = controller.request.format.try(:ref)
if suffix && suffix != :html
action += ".#{suffix}"
......
......@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-05-14 10:49+0200\n"
"PO-Revision-Date: 2018-05-14 10:49+0200\n"
"POT-Creation-Date: 2018-05-15 15:05+0200\n"
"PO-Revision-Date: 2018-05-15 15:05+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -53,6 +53,16 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
msgid "%d staged change"
msgid_plural "%d staged changes"
msgstr[0] ""
msgstr[1] ""
msgid "%d unstaged change"
msgid_plural "%d unstaged changes"
msgstr[0] ""
msgstr[1] ""
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] ""
......@@ -101,6 +111,9 @@ msgstr ""
msgid "%{title} changes"
msgstr ""
msgid "%{unstaged} unstaged and %{staged} staged changes"
msgstr ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
......@@ -207,6 +220,9 @@ msgstr ""
msgid "Active"
msgstr ""
msgid "Active Sessions"
msgstr ""
msgid "Activity"
msgstr ""
......@@ -315,6 +331,9 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
msgid "An error occurred while dismissing the alert. Refresh the page and try again."
msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr ""
......@@ -1022,6 +1041,9 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
msgstr ""
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
......@@ -1145,6 +1167,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
msgstr ""
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
......@@ -1235,6 +1260,9 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
msgid "ClusterIntegration|sign up"
msgstr ""
msgid "Collapse"
msgstr ""
......@@ -1359,6 +1387,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
msgid "Configure push mirrors."
msgstr ""
msgid "Configure storage path and circuit breaker settings."
msgstr ""
......@@ -1524,6 +1555,9 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
msgid "Created"
msgstr ""
msgid "Cron Timezone"
msgstr ""
......@@ -1595,6 +1629,54 @@ msgstr[1] ""
msgid "Deploy Keys"
msgstr ""
msgid "DeployKeys|+%{count} others"
msgstr ""
msgid "DeployKeys|Current project"
msgstr ""
msgid "DeployKeys|Deploy key"
msgstr ""
msgid "DeployKeys|Enabled deploy keys"
msgstr ""
msgid "DeployKeys|Error enabling deploy key"
msgstr ""
msgid "DeployKeys|Error getting deploy keys"
msgstr ""
msgid "DeployKeys|Error removing deploy key"
msgstr ""
msgid "DeployKeys|Expand %{count} other projects"
msgstr ""
msgid "DeployKeys|Loading deploy keys"
msgstr ""
msgid "DeployKeys|No deploy keys found. Create one with the form above."
msgstr ""
msgid "DeployKeys|Privately accessible deploy keys"
msgstr ""
msgid "DeployKeys|Project usage"
msgstr ""
msgid "DeployKeys|Publicly accessible deploy keys"
msgstr ""
msgid "DeployKeys|Read access only"
msgstr ""
msgid "DeployKeys|Write access allowed"
msgstr ""
msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
msgstr ""
msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
msgstr ""
......@@ -1751,9 +1833,6 @@ msgstr ""
msgid "Edit files in the editor and commit changes here"
msgstr ""
msgid "Editing"
msgstr ""
msgid "Email"
msgstr ""
......@@ -1793,6 +1872,9 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
msgid "Environments"
msgstr ""
msgid "Environments|An error occurred while fetching the environments."
msgstr ""
......@@ -2005,6 +2087,9 @@ msgstr ""
msgid "GPG Keys"
msgstr ""
msgid "General"
msgstr ""
msgid "Generate a default set of labels"
msgstr ""
......@@ -2053,6 +2138,9 @@ msgstr ""
msgid "Got it!"
msgstr ""
msgid "Graph"
msgstr ""
msgid "Group CI/CD settings"
msgstr ""
......@@ -2166,6 +2254,18 @@ msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
msgid "IDE|Commit"
msgstr ""
msgid "IDE|Edit"
msgstr ""
msgid "IDE|Go back"
msgstr ""
msgid "IDE|Review"
msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
msgstr ""
......@@ -2199,6 +2299,9 @@ msgstr ""
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Integrations"
msgstr ""
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
......@@ -2336,6 +2439,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
msgid "Latest changes"
msgstr ""
msgid "Learn more"
msgstr ""
......@@ -2360,6 +2466,9 @@ msgstr ""
msgid "Leave project"
msgstr ""
msgid "List"
msgstr ""
msgid "List your GitHub repositories"
msgstr ""
......@@ -2462,6 +2571,9 @@ msgstr ""
msgid "Milestone"
msgstr ""
msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone"
msgstr ""
......@@ -2719,6 +2831,9 @@ msgstr ""
msgid "Opens in a new window"
msgstr ""
msgid "Operations"
msgstr ""
msgid "Options"
msgstr ""
......@@ -2878,25 +2993,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
msgid "Pipeline|Existing branch name, tag"
msgid "Pipeline|Create for"
msgstr ""
msgid "Pipeline|Retry pipeline"
msgid "Pipeline|Create pipeline"
msgstr ""
msgid "Pipeline|Retry pipeline #%{pipelineId}?"
msgid "Pipeline|Existing branch name or tag"
msgstr ""
msgid "Pipeline|Run Pipeline"
msgstr ""
msgid "Pipeline|Run on"
msgstr ""
msgid "Pipeline|Run pipeline"
msgid "Pipeline|Search branches"
msgstr ""
msgid "Pipeline|Search branches"
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
......@@ -2905,7 +3017,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
......@@ -3022,6 +3134,9 @@ msgstr ""
msgid "Progress"
msgstr ""
msgid "Project"
msgstr ""
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
......@@ -3073,9 +3188,6 @@ msgstr ""
msgid "ProjectLifecycle|Stage"
msgstr ""
msgid "ProjectNetworkGraph|Graph"
msgstr ""
msgid "Projects"
msgstr ""
......@@ -3226,6 +3338,9 @@ msgstr ""
msgid "Register and see your runners for this group."
msgstr ""
msgid "Registry"
msgstr ""
msgid "Related Commits"
msgstr ""
......@@ -3268,6 +3383,9 @@ msgstr ""
msgid "Repository maintenance"
msgstr ""
msgid "Repository mirror settings"
msgstr ""
msgid "Repository storage"
msgstr ""
......@@ -3363,6 +3481,9 @@ msgstr ""
msgid "Search"
msgstr ""
msgid "Search branches"
msgstr ""
msgid "Search branches and tags"
msgstr ""
......@@ -3405,6 +3526,9 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
msgid "Select source branch"
msgstr ""
msgid "Select target branch"
msgstr ""
......@@ -3937,6 +4061,9 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
msgid "This option is disabled while you still have unstaged changes"
msgstr ""
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
......@@ -4253,9 +4380,6 @@ msgstr ""
msgid "Verified"
msgstr ""
msgid "View and edit lines"
msgstr ""
msgid "View file @ "
msgstr ""
......@@ -4373,6 +4497,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
msgid "WikiPageConfirmDelete|Delete page"
msgstr ""
msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
msgstr ""
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
msgstr ""
......
#!/bin/bash
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'@'%';
GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
FLUSH PRIVILEGES;
......
#!/bin/bash
psql -h postgres -U postgres postgres <<EOF
DROP DATABASE IF EXISTS gitlabhq_test;
CREATE DATABASE gitlabhq_test;
CREATE USER gitlab;
GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gitlab;
EOF
......@@ -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
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
bundle exec rake db:drop db:create db:schema:load db:migrate
if [ "$GITLAB_DATABASE" = "mysql" ]; then
bundle exec rake add_limits_mysql
fi
setup_db
elif getent hosts postgres || getent hosts mysql; then
setup_db_user_only
fi
......@@ -12,3 +12,21 @@ retry() {
done
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
end
describe "as diff" do
include_examples "export as", :diff
let(:format) { :diff }
it "triggers workhorse to serve the request" do
go(id: commit.id, format: :diff)
it "should really only be a git diff" do
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
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
end
end
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
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
......
......@@ -19,11 +19,11 @@ describe Projects::Settings::CiCdController do
end
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(:group) { create(:group, runners: [group_runner], parent: parent_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) }
it 'sets assignable project runners only' do
......@@ -31,7 +31,7 @@ describe Projects::Settings::CiCdController do
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
......
......@@ -5,18 +5,41 @@ describe 'Invites' do
let(:owner) { create(:user, name: 'John Doe') }
let(:group) { create(:group, name: 'Owned') }
let(:project) { create(:project, :repository, namespace: group) }
let(:invite) { group.group_members.invite.last }
let(:group_invite) { group.group_members.invite.last }
before do
project.add_master(owner)
group.add_user(owner, Gitlab::Access::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
context 'when signed out' do
before do
visit invite_path(invite.raw_invite_token)
visit invite_path(group_invite.raw_invite_token)
end
it 'renders sign in page with sign in notice' do
......@@ -25,12 +48,9 @@ describe 'Invites' do
end
it 'sign in and redirects to invitation page' do
fill_in 'user_login', with: user.email
fill_in 'user_password', with: user.password
check 'user_remember_me'
click_button 'Sign in'
fill_in_sign_in_form(user)
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(
'You have been invited by John Doe to join group Owned as Developer.'
)
......@@ -45,7 +65,7 @@ describe 'Invites' do
end
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.')
end
end
......@@ -53,7 +73,7 @@ describe 'Invites' do
describe 'accepting the invitation' do
before do
sign_in(user)
visit invite_path(invite.raw_invite_token)
visit invite_path(group_invite.raw_invite_token)
end
it 'grants access and redirects to group page' do
......@@ -69,7 +89,7 @@ describe 'Invites' do
context 'when signed in' do
before do
sign_in(user)
visit invite_path(invite.raw_invite_token)
visit invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to dashboard' do
......@@ -83,7 +103,7 @@ describe 'Invites' do
context 'when signed out' do
before do
visit decline_invite_path(invite.raw_invite_token)
visit decline_invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to sign in page' do
......@@ -94,4 +114,72 @@ describe 'Invites' do
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
......@@ -17,6 +17,7 @@
"required": [
"id",
"color",
"text_color",
"description",
"title",
"priority"
......@@ -29,6 +30,7 @@
},
"description": { "type": ["string", "null"] },
"title": { "type": "string" },
"title": { "text_color": "string" },
"priority": { "type": ["integer", "null"] }
}
},
......
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
it_should_behave_like '#stats'
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
it { expect(commit.has_zero_stats?).to eq(false) }
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
let(:hash) { commit.to_hash }
subject { hash }
......
......@@ -180,11 +180,11 @@ describe Gitlab::Metrics::WebTransaction do
end
context 'when request goes to ActionController' do
let(:request_format) { :html }
let(:request) { double(:request, format: double(:format, ref: :html)) }
before do
klass = double(:klass, name: 'TestController')
controller = double(:controller, class: klass, action_name: 'show', request_format: request_format)
controller = double(:controller, class: klass, action_name: 'show', request: request)
env['action_controller.instance'] = controller
end
......@@ -195,7 +195,7 @@ describe Gitlab::Metrics::WebTransaction do
end
context 'when the response content type is not :html' do
let(:request_format) { :json }
let(:request) { double(:request, format: double(:format, ref: :json)) }
it 'appends the mime type to the transaction action' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' })
......
......@@ -594,7 +594,7 @@ describe Notify 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_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.invite_token
end
......
......@@ -5,7 +5,7 @@ describe Appearance do
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
let!(:appearance) { create(:appearance) }
......@@ -41,4 +41,12 @@ describe Appearance do
expect(new_row.valid?).to eq(false)
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
......@@ -626,62 +626,26 @@ describe Ci::Runner do
end
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(:another_project) { create(:project) }
before 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
context 'with already assigned project' do
subject { described_class.assignable_for(project) }
it { is_expected.to be_empty }
end
context 'does give a specific runner' do
context 'with a different project' do
subject { described_class.assignable_for(another_project) }
it { is_expected.to contain_exactly(runner) }
end
end
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
it { is_expected.to include(unlocked_project_runner) }
it { is_expected.not_to include(group_runner) }
it { is_expected.not_to include(locked_project_runner) }
it { is_expected.not_to include(instance_runner) }
end
end
......
......@@ -182,7 +182,6 @@ eos
it { is_expected.to respond_to(:date) }
it { is_expected.to respond_to(:diffs) }
it { is_expected.to respond_to(:id) }
it { is_expected.to respond_to(:to_patch) }
end
describe '#closes_issues' do
......
......@@ -565,4 +565,10 @@ describe CommitStatus do
it_behaves_like 'commit status enqueued'
end
end
describe '#present' do
subject { commit_status.present }
it { is_expected.to be_a(CommitStatusPresenter) }
end
end
......@@ -78,4 +78,10 @@ describe GenericCommitStatus do
it { is_expected.not_to be_nil }
end
end
describe '#present' do
subject { generic_commit_status.present }
it { is_expected.to be_a(GenericCommitStatusPresenter) }
end
end
......@@ -15,7 +15,7 @@ describe Group do
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(: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_many(:custom_attributes).class_name('GroupCustomAttribute') }
it { is_expected.to have_many(:badges).class_name('GroupBadge') }
......@@ -691,4 +691,12 @@ describe Group do
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
......@@ -76,7 +76,7 @@ describe Project do
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(: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(:members_and_requesters) }
it { is_expected.to have_many(:clusters) }
......@@ -3739,4 +3739,12 @@ describe Project do
it { is_expected.to be_nil }
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
......@@ -990,7 +990,6 @@ describe Repository do
subject { repository.add_branch(user, branch_name, target) }
context 'with Gitaly enabled' do
it "calls Gitaly's OperationService" do
expect_any_instance_of(Gitlab::GitalyClient::OperationService)
.to receive(:user_create_branch).with(branch_name, user, target)
......@@ -1014,45 +1013,6 @@ describe Repository do
end
end
context 'with Gitaly disabled', :disable_gitaly do
context 'when pre hooks were successful' do
it 'runs without errors' do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
expect { subject }.not_to raise_error
end
it 'creates the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
expect(subject.name).to eq(branch_name)
end
it 'calls the after_create_branch hook' do
expect(repository).to receive(:after_create_branch)
subject
end
end
context 'when pre hooks failed' do
it 'gets an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
it 'does not create the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
expect(repository.find_branch(branch_name)).to be_nil
end
end
end
end
describe '#find_branch' do
context 'fresh_repo is true' do
it 'delegates the call to raw_repository' do
......
......@@ -39,7 +39,7 @@ describe User do
it { is_expected.to have_many(:builds).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(: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(:custom_attributes).class_name('UserCustomAttribute') }
......@@ -1223,6 +1223,24 @@ describe User do
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
let(:user) { create(:user) }
......@@ -1786,28 +1804,54 @@ describe User do
end
end
describe '#ci_authorized_runners' do
describe '#ci_owned_runners' do
let(:user) { create(:user) }
let(:runner) { create(:ci_runner) }
let(:runner_1) { create(:ci_runner) }
let(:runner_2) { create(:ci_runner) }
before do
project.runners << runner
end
context 'without any projects' do
let(:project) { create(:project) }
context 'without any projects nor groups' do
let!(:project) { create(:project, runners: [runner_1]) }
let!(:group) { create(:group) }
it 'does not load' do
expect(user.ci_authorized_runners).to be_empty
expect(user.ci_owned_runners).to be_empty
end
end
context 'with personal projects runners' do
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
expect(user.ci_authorized_runners).to contain_exactly(runner)
expect(user.ci_owned_runners).to contain_exactly(runner_1, runner_2)
end
end
......@@ -1818,7 +1862,7 @@ describe User do
end
it 'loads' do
expect(user.ci_authorized_runners).to contain_exactly(runner)
expect(user.ci_owned_runners).to contain_exactly(runner_1)
end
end
......@@ -1828,14 +1872,28 @@ describe User do
end
it 'does not load' do
expect(user.ci_authorized_runners).to be_empty
expect(user.ci_owned_runners).to be_empty
end
end
end
context 'with groups projects runners' do
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)
group.add_user(user, access)
......@@ -1845,7 +1903,7 @@ describe User do
end
context 'with other projects runners' do
let(:project) { create(:project) }
let!(:project) { create(:project, runners: [runner_1]) }
def add_user(access)
project.add_role(user, access)
......@@ -1858,7 +1916,7 @@ describe User do
let(:group) { create(:group) }
let(:another_user) { create(:user) }
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)
group.add_user(user, access)
......@@ -2769,4 +2827,12 @@ describe User do
expect { user.increment_failed_attempts! }.not_to change(user, :failed_attempts)
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
......@@ -10,7 +10,7 @@ describe Ci::BuildPresenter do
end
it 'inherits from Gitlab::View::Presenter::Delegated' do
expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
expect(described_class.ancestors).to include(Gitlab::View::Presenter::Delegated)
end
describe '#initialize' do
......
require 'spec_helper'
describe CommitStatusPresenter do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
subject(:presenter) do
described_class.new(build)
end
it 'inherits from Gitlab::View::Presenter::Delegated' do
expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
end
end
......@@ -27,7 +27,7 @@ describe API::Runners do
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
# Set project access for users
......@@ -48,7 +48,7 @@ describe API::Runners do
expect(json_response).to be_an Array
expect(json_response[0]).to have_key('ip_address')
expect(descriptions).to contain_exactly(
'Project runner', 'Two projects runner'
'Project runner', 'Two projects runner', 'Group runner'
)
expect(shared).to be_falsey
end
......@@ -592,6 +592,15 @@ describe API::Runners do
end.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(201)
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
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
......@@ -36,16 +36,17 @@ describe 'layouts/nav/sidebar/_project' do
expect(rendered).to have_text 'Registry'
end
it 'highlights only one tab' do
it 'highlights sidebar item and flyout' do
render
expect(rendered).to have_css('.active', count: 1)
expect(rendered).to have_css('.sidebar-top-level-items > li.active', count: 1)
expect(rendered).to have_css('.is-fly-out-only > li.active', count: 1)
end
it 'highlights container registry tab only' do
it 'highlights container registry tab' do
render
expect(rendered).to have_css('.active', text: 'Registry')
expect(rendered).to have_css('.sidebar-top-level-items > li.active', text: 'Registry')
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