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: ...@@ -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
......
...@@ -4,9 +4,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of ...@@ -4,9 +4,4 @@ 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 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. 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. ...@@ -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 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 ## 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). 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 @@ ...@@ -179,7 +179,7 @@
role="row" role="row"
> >
<div <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" role="gridcell"
> >
<div> <div>
......
...@@ -32,17 +32,17 @@ export default { ...@@ -32,17 +32,17 @@ export default {
<template> <template>
<div :class="className"> <div :class="className">
{{ actionText }} {{ actionText }}
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom"
/>
<template v-if="editedBy"> <template v-if="editedBy">
by {{ s__('ByAuthor|by') }}
<a <a
:href="editedBy.path" :href="editedBy.path"
class="js-vue-author author_link"> class="js-vue-author author_link">
{{ editedBy.name }} {{ editedBy.name }}
</a> </a>
</template> </template>
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom"
/>
</div> </div>
</template> </template>
...@@ -62,6 +62,21 @@ export default { ...@@ -62,6 +62,21 @@ export default {
<template> <template>
<div class="note-header-info"> <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"> <a :href="author.path">
<span class="note-header-author-name">{{ author.name }}</span> <span class="note-header-author-name">{{ author.name }}</span>
<span class="note-headline-light"> <span class="note-headline-light">
...@@ -78,10 +93,13 @@ export default { ...@@ -78,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"
...@@ -95,20 +113,5 @@ export default { ...@@ -95,20 +113,5 @@ export default {
</i> </i>
</span> </span>
</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> </div>
</template> </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 '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>
...@@ -67,7 +67,8 @@ ...@@ -67,7 +67,8 @@
padding: 8px 40px; padding: 8px 40px;
} }
.embed-toggle { .embed-toggle,
.snippet-clipboard-btn {
height: 35px; height: 35px;
} }
} }
@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;
......
...@@ -407,10 +407,6 @@ ul.notes { ...@@ -407,10 +407,6 @@ ul.notes {
.note-header { .note-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@include notes-media('max', $screen-xs-max) {
flex-flow: row wrap;
}
} }
.note-header-info { .note-header-info {
...@@ -459,6 +455,10 @@ ul.notes { ...@@ -459,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;
} }
...@@ -473,11 +473,6 @@ ul.notes { ...@@ -473,11 +473,6 @@ ul.notes {
margin-left: 10px; margin-left: 10px;
color: $gray-darkest; color: $gray-darkest;
@include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
}
.btn-group > .discussion-next-btn { .btn-group > .discussion-next-btn {
margin-left: -1px; margin-left: -1px;
} }
......
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
...@@ -257,6 +257,7 @@ module ProjectsHelper ...@@ -257,6 +257,7 @@ module ProjectsHelper
if project.builds_enabled? && can?(current_user, :read_pipeline, project) if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines nav_tabs << :pipelines
nav_tabs << :operations
end end
if project.external_issue_tracker if project.external_issue_tracker
......
...@@ -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
......
...@@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base
include HasStatus include HasStatus
include Importable include Importable
include AfterCommitQueue include AfterCommitQueue
include Presentable
self.table_name = 'ci_builds' 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 ...@@ -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'
......
...@@ -31,7 +31,8 @@ class List < ActiveRecord::Base ...@@ -31,7 +31,8 @@ class List < ActiveRecord::Base
if options.key?(:label) if options.key?(:label)
json[:label] = label.as_json( json[:label] = label.as_json(
project: board.project, project: board.project,
only: [:id, :title, :description, :color] only: [:id, :title, :description, :color],
methods: [:text_color]
) )
end end
end end
......
...@@ -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
module Ci module Ci
class BuildPresenter < Gitlab::View::Presenter::Delegated class BuildPresenter < CommitStatusPresenter
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 erased_by_user? def erased_by_user?
# Build can be erased through API, therefore it does not have # Build can be erased through API, therefore it does not have
# `erased_by` user assigned in that case. # `erased_by` user assigned in that case.
...@@ -44,14 +33,6 @@ module Ci ...@@ -44,14 +33,6 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}" "#{subject.name} - #{detailed_status.status_tooltip}"
end end
def callout_failure_message
CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
end
def recoverable?
failed? && !unrecoverable?
end
private private
def tooltip_for_badge def tooltip_for_badge
...@@ -61,9 +42,5 @@ module Ci ...@@ -61,9 +42,5 @@ module Ci
def detailed_status def detailed_status
@detailed_status ||= subject.detailed_status(user) @detailed_status ||= subject.detailed_status(user)
end end
def unrecoverable?
script_failure? || missing_dependency_failure?
end
end 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 @@ ...@@ -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.pull-right %span.light.pull-right
= number_with_delimiter(ForkedProjectLink.count) = approximate_count_with_delimiters(ForkedProjectLink)
%p %p
Issues Issues
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(Issue.count) = approximate_count_with_delimiters(Issue)
%p %p
Merge Requests Merge Requests
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(MergeRequest.count) = approximate_count_with_delimiters(MergeRequest)
%p %p
Notes Notes
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(Note.count) = approximate_count_with_delimiters(Note)
%p %p
Snippets Snippets
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(Snippet.count) = approximate_count_with_delimiters(Snippet)
%p %p
SSH Keys SSH Keys
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(Key.count) = approximate_count_with_delimiters(Key)
%p %p
Milestones Milestones
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(Milestone.count) = approximate_count_with_delimiters(Milestone)
%p %p
Active Users Active Users
%span.light.pull-right %span.light.pull-right
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= icon("chevron-up") = icon("chevron-up")
- else - else
= icon("chevron-down") = icon("chevron-down")
Toggle discussion = _('Toggle discussion')
= link_to_member(@project, discussion.author, avatar: false) = link_to_member(@project, discussion.author, avatar: false)
.inline.discussion-headline-light .inline.discussion-headline-light
......
...@@ -13,13 +13,13 @@ ...@@ -13,13 +13,13 @@
.nav-icon-container .nav-icon-container
= sprite_icon('project') = sprite_icon('project')
%span.nav-item-name %span.nav-item-name
Project = _('Project')
%ul.sidebar-sub-level-items %ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
= link_to project_path(@project) do = link_to project_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Overview') } = _('Overview')
%li.divider.fly-out-top-item %li.divider.fly-out-top-item
= nav_link(path: 'projects#show') do = nav_link(path: 'projects#show') do
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
...@@ -40,45 +40,45 @@ ...@@ -40,45 +40,45 @@
.nav-icon-container .nav-icon-container
= sprite_icon('doc_text') = sprite_icon('doc_text')
%span.nav-item-name %span.nav-item-name
Repository = _('Repository')
%ul.sidebar-sub-level-items %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 = 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 = link_to project_tree_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Repository') } = _('Repository')
%li.divider.fly-out-top-item %li.divider.fly-out-top-item
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_tree_path(@project) do = link_to project_tree_path(@project) do
#{ _('Files') } = _('Files')
= nav_link(controller: [:commit, :commits]) do = nav_link(controller: [:commit, :commits]) do
= link_to project_commits_path(@project, current_ref) do = link_to project_commits_path(@project, current_ref) do
#{ _('Commits') } = _('Commits')
= nav_link(html_options: {class: branches_tab_class}) do = nav_link(html_options: {class: branches_tab_class}) do
= link_to project_branches_path(@project) do = link_to project_branches_path(@project) do
#{ _('Branches') } = _('Branches')
= nav_link(controller: [:tags, :releases]) do = nav_link(controller: [:tags, :releases]) do
= link_to project_tags_path(@project) do = link_to project_tags_path(@project) do
#{ _('Tags') } = _('Tags')
= nav_link(path: 'graphs#show') do = nav_link(path: 'graphs#show') do
= link_to project_graph_path(@project, current_ref) do = link_to project_graph_path(@project, current_ref) do
#{ _('Contributors') } = _('Contributors')
= nav_link(controller: %w(network)) do = nav_link(controller: %w(network)) do
= link_to project_network_path(@project, current_ref) do = link_to project_network_path(@project, current_ref) do
#{ s_('ProjectNetworkGraph|Graph') } = _('Graph')
= nav_link(controller: :compare) do = nav_link(controller: :compare) do
= link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
#{ _('Compare') } = _('Compare')
= nav_link(path: 'graphs#charts') do = nav_link(path: 'graphs#charts') do
= link_to charts_project_graph_path(@project, current_ref) do = link_to charts_project_graph_path(@project, current_ref) do
#{ _('Charts') } = _('Charts')
- if project_nav_tab? :issues - if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
.nav-icon-container .nav-icon-container
= sprite_icon('issues') = sprite_icon('issues')
%span.nav-item-name %span.nav-item-name
Issues = _('Issues')
- if @project.issues_enabled? - if @project.issues_enabled?
%span.badge.count.issue_counter %span.badge.count.issue_counter
= number_with_delimiter(@project.open_issues_count) = number_with_delimiter(@project.open_issues_count)
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
= nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do
= link_to project_issues_path(@project) do = link_to project_issues_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Issues') } = _('Issues')
- if @project.issues_enabled? - if @project.issues_enabled?
%span.badge.count.issue_counter.fly-out-badge %span.badge.count.issue_counter.fly-out-badge
= number_with_delimiter(@project.open_issues_count) = number_with_delimiter(@project.open_issues_count)
...@@ -103,7 +103,7 @@ ...@@ -103,7 +103,7 @@
= nav_link(controller: :issues, action: :index) do = nav_link(controller: :issues, action: :index) do
= link_to project_issues_path(@project), title: 'Issues' do = link_to project_issues_path(@project), title: 'Issues' do
%span %span
List = _('List')
= nav_link(controller: :boards) do = nav_link(controller: :boards) do
= link_to project_boards_path(@project), title: boards_link_text do = link_to project_boards_path(@project), title: boards_link_text do
...@@ -113,12 +113,12 @@ ...@@ -113,12 +113,12 @@
= nav_link(controller: :labels) do = nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: 'Labels' do = link_to project_labels_path(@project), title: 'Labels' do
%span %span
Labels = _('Labels')
= nav_link(controller: :milestones) do = nav_link(controller: :milestones) do
= link_to project_milestones_path(@project), title: 'Milestones' do = link_to project_milestones_path(@project), title: 'Milestones' do
%span %span
Milestones = _('Milestones')
- if project_nav_tab? :external_issue_tracker - if project_nav_tab? :external_issue_tracker
= nav_link do = nav_link do
- issue_tracker = @project.external_issue_tracker - issue_tracker = @project.external_issue_tracker
...@@ -139,54 +139,75 @@ ...@@ -139,54 +139,75 @@
.nav-icon-container .nav-icon-container
= sprite_icon('git-merge') = sprite_icon('git-merge')
%span.nav-item-name %span.nav-item-name
Merge Requests = _('Merge Requests')
%span.badge.count.merge_counter.js-merge-counter %span.badge.count.merge_counter.js-merge-counter
= number_with_delimiter(@project.open_merge_requests_count) = number_with_delimiter(@project.open_merge_requests_count)
%ul.sidebar-sub-level-items.is-fly-out-only %ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
= link_to project_merge_requests_path(@project) do = link_to project_merge_requests_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Merge Requests') } = _('Merge Requests')
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge %span.badge.count.merge_counter.js-merge-counter.fly-out-badge
= number_with_delimiter(@project.open_merge_requests_count) = number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines - 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 = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container .nav-icon-container
= sprite_icon('pipeline') = sprite_icon('pipeline')
%span.nav-item-name %span.nav-item-name
CI / CD = _('CI / CD')
%ul.sidebar-sub-level-items %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 = link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('CI / CD') } = _('CI / CD')
%li.divider.fly-out-top-item %li.divider.fly-out-top-item
- if project_nav_tab? :pipelines - if project_nav_tab? :pipelines
= nav_link(path: ['pipelines#index', 'pipelines#show']) do = nav_link(path: ['pipelines#index', 'pipelines#show']) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span %span
Pipelines = _('Pipelines')
- if project_nav_tab? :builds - if project_nav_tab? :builds
= nav_link(controller: [:jobs, :artifacts]) do = nav_link(controller: [:jobs, :artifacts]) do
= link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span %span
Jobs = _('Jobs')
- if project_nav_tab? :pipelines - if project_nav_tab? :pipelines
= nav_link(controller: :pipeline_schedules) do = nav_link(controller: :pipeline_schedules) do
= link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do
%span %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 - if project_nav_tab? :environments
= nav_link(controller: :environments) do = nav_link(controller: :environments) do
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span %span
Environments = _('Environments')
- if project_nav_tab? :clusters - if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project) - show_cluster_hint = show_gke_cluster_integration_callout?(@project)
...@@ -217,19 +238,18 @@ ...@@ -217,19 +238,18 @@
%span= _("Got it!") %span= _("Got it!")
= sprite_icon('thumb-up') = 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 - if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do = nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
.nav-icon-container .nav-icon-container
= sprite_icon('disk') = sprite_icon('disk')
%span.nav-item-name %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 - if project_nav_tab? :wiki
= nav_link(controller: :wikis) do = nav_link(controller: :wikis) do
...@@ -237,12 +257,12 @@ ...@@ -237,12 +257,12 @@
.nav-icon-container .nav-icon-container
= sprite_icon('book') = sprite_icon('book')
%span.nav-item-name %span.nav-item-name
Wiki = _('Wiki')
%ul.sidebar-sub-level-items.is-fly-out-only %ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
= link_to get_project_wiki_path(@project) do = link_to get_project_wiki_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Wiki') } = _('Wiki')
- if project_nav_tab? :snippets - if project_nav_tab? :snippets
= nav_link(controller: :snippets) do = nav_link(controller: :snippets) do
...@@ -250,12 +270,12 @@ ...@@ -250,12 +270,12 @@
.nav-icon-container .nav-icon-container
= sprite_icon('snippet') = sprite_icon('snippet')
%span.nav-item-name %span.nav-item-name
Snippets = _('Snippets')
%ul.sidebar-sub-level-items.is-fly-out-only %ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
= link_to project_snippets_path(@project) do = link_to project_snippets_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Snippets') } = _('Snippets')
- if project_nav_tab? :settings - 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 = 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 @@ ...@@ -263,7 +283,7 @@
.nav-icon-container .nav-icon-container
= sprite_icon('settings') = sprite_icon('settings')
%span.nav-item-name.qa-settings-item %span.nav-item-name.qa-settings-item
Settings = _('Settings')
%ul.sidebar-sub-level-items %ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project) - can_edit = can?(current_user, :admin_project, @project)
...@@ -271,16 +291,16 @@ ...@@ -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 = 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 = link_to edit_project_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Settings') } = _('Settings')
%li.divider.fly-out-top-item %li.divider.fly-out-top-item
= nav_link(path: %w[projects#edit]) do = nav_link(path: %w[projects#edit]) do
= link_to edit_project_path(@project), title: 'General' do = link_to edit_project_path(@project), title: 'General' do
%span %span
General = _('General')
= nav_link(controller: :project_members) do = nav_link(controller: :project_members) do
= link_to project_project_members_path(@project), title: 'Members' do = link_to project_project_members_path(@project), title: 'Members' do
%span %span
Members = _('Members')
- if can_edit - if can_edit
= nav_link(controller: :badges) do = nav_link(controller: :badges) do
= link_to project_settings_badges_path(@project), title: _('Badges') do = link_to project_settings_badges_path(@project), title: _('Badges') do
...@@ -290,21 +310,21 @@ ...@@ -290,21 +310,21 @@
= nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do = link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span %span
Integrations = _('Integrations')
= nav_link(controller: :repository) do = nav_link(controller: :repository) do
= link_to project_settings_repository_path(@project), title: 'Repository' do = link_to project_settings_repository_path(@project), title: 'Repository' do
%span %span
Repository = _('Repository')
- if @project.feature_available?(:builds, current_user) - if @project.feature_available?(:builds, current_user)
= nav_link(controller: :ci_cd) do = nav_link(controller: :ci_cd) do
= link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do
%span %span
CI / CD = _('CI / CD')
- if @project.pages_available? - if @project.pages_available?
= nav_link(controller: :pages) do = nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: 'Pages' do = link_to project_pages_path(@project), title: 'Pages' do
%span %span
Pages = _('Pages')
- else - else
= nav_link(controller: :project_members) do = nav_link(controller: :project_members) do
...@@ -312,12 +332,12 @@ ...@@ -312,12 +332,12 @@
.nav-icon-container .nav-icon-container
= sprite_icon('users') = sprite_icon('users')
%span.nav-item-name %span.nav-item-name
Members = _('Members')
%ul.sidebar-sub-level-items.is-fly-out-only %ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do = nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
= link_to project_project_members_path(@project) do = link_to project_project_members_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Members') } = _('Members')
= render 'shared/sidebar_toggle_button' = render 'shared/sidebar_toggle_button'
......
...@@ -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
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
":title" => '(list.label ? list.label.description : "")', ":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" }, data: { container: "body", placement: "bottom" },
class: "label color-label title board-title-text", 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 }} {{ list.title }}
- if can?(current_user, :admin_list, current_board_parent) - if can?(current_user, :admin_list, current_board_parent)
......
...@@ -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?
......
...@@ -45,6 +45,6 @@ ...@@ -45,6 +45,6 @@
%strong.embed-toggle-list-item= _("Share") %strong.embed-toggle-list-item= _("Share")
%input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed } %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
.input-group-btn .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) = sprite_icon('duplicate', size: 16)
.clearfix .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 ...@@ -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 **Fork** button. Soon you should have a project under your namespace with the
necessary files. 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 ## Setup your own cluster on Google Kubernetes Engine
If you do not already have a Google Cloud account, create one at If you do not already have a Google Cloud account, create one at
......
...@@ -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.
......
...@@ -776,13 +776,9 @@ module Gitlab ...@@ -776,13 +776,9 @@ module Gitlab
end end
def add_branch(branch_name, user:, target:) def add_branch(branch_name, user:, target:)
gitaly_migrate(:operation_user_create_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| gitaly_operation_client.user_create_branch(branch_name, user, target)
if is_enabled rescue GRPC::FailedPrecondition => ex
gitaly_add_branch(branch_name, user, target) raise InvalidRef, ex
else
rugged_add_branch(branch_name, user, target)
end
end
end end
def add_tag(tag_name, user:, target:, message: nil) def add_tag(tag_name, user:, target:, message: nil)
...@@ -2197,22 +2193,6 @@ module Gitlab ...@@ -2197,22 +2193,6 @@ module Gitlab
end end
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:) def rugged_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch( OperationService.new(user, self).with_branch(
branch_name, branch_name,
......
...@@ -28,7 +28,11 @@ module Gitlab ...@@ -28,7 +28,11 @@ module Gitlab
controller = @env[CONTROLLER_KEY] controller = @env[CONTROLLER_KEY]
action = "#{controller.action_name}" 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 if suffix && suffix != :html
action += ".#{suffix}" action += ".#{suffix}"
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-05-14 10:49+0200\n" "POT-Creation-Date: 2018-05-15 15:05+0200\n"
"PO-Revision-Date: 2018-05-14 10:49+0200\n" "PO-Revision-Date: 2018-05-15 15:05+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -53,6 +53,16 @@ msgid_plural "%d metrics" ...@@ -53,6 +53,16 @@ msgid_plural "%d metrics"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" 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 "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues." msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "" msgstr[0] ""
...@@ -101,6 +111,9 @@ msgstr "" ...@@ -101,6 +111,9 @@ msgstr ""
msgid "%{title} changes" msgid "%{title} changes"
msgstr "" msgstr ""
msgid "%{unstaged} unstaged and %{staged} staged changes"
msgstr ""
msgid "(checkout the %{link} for information on how to install it)." msgid "(checkout the %{link} for information on how to install it)."
msgstr "" msgstr ""
...@@ -207,6 +220,9 @@ msgstr "" ...@@ -207,6 +220,9 @@ msgstr ""
msgid "Active" msgid "Active"
msgstr "" msgstr ""
msgid "Active Sessions"
msgstr ""
msgid "Activity" msgid "Activity"
msgstr "" msgstr ""
...@@ -315,6 +331,9 @@ msgstr "" ...@@ -315,6 +331,9 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription" msgid "An error occurred when toggling the notification subscription"
msgstr "" 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." msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr "" msgstr ""
...@@ -1022,6 +1041,9 @@ msgstr "" ...@@ -1022,6 +1041,9 @@ msgstr ""
msgid "ClusterIntegration|Environment scope" msgid "ClusterIntegration|Environment scope"
msgstr "" 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" msgid "ClusterIntegration|GitLab Integration"
msgstr "" msgstr ""
...@@ -1145,6 +1167,9 @@ msgstr "" ...@@ -1145,6 +1167,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration." msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr "" msgstr ""
msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
msgstr ""
msgid "ClusterIntegration|Remove Kubernetes cluster integration" msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr "" msgstr ""
...@@ -1235,6 +1260,9 @@ msgstr "" ...@@ -1235,6 +1260,9 @@ msgstr ""
msgid "ClusterIntegration|properly configured" msgid "ClusterIntegration|properly configured"
msgstr "" msgstr ""
msgid "ClusterIntegration|sign up"
msgstr ""
msgid "Collapse" msgid "Collapse"
msgstr "" msgstr ""
...@@ -1359,6 +1387,9 @@ msgstr "" ...@@ -1359,6 +1387,9 @@ msgstr ""
msgid "Configure limits for web and API requests." msgid "Configure limits for web and API requests."
msgstr "" msgstr ""
msgid "Configure push mirrors."
msgstr ""
msgid "Configure storage path and circuit breaker settings." msgid "Configure storage path and circuit breaker settings."
msgstr "" msgstr ""
...@@ -1524,6 +1555,9 @@ msgstr "" ...@@ -1524,6 +1555,9 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token" msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "" msgstr ""
msgid "Created"
msgstr ""
msgid "Cron Timezone" msgid "Cron Timezone"
msgstr "" msgstr ""
...@@ -1595,6 +1629,54 @@ msgstr[1] "" ...@@ -1595,6 +1629,54 @@ msgstr[1] ""
msgid "Deploy Keys" msgid "Deploy Keys"
msgstr "" 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})" msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
msgstr "" msgstr ""
...@@ -1751,9 +1833,6 @@ msgstr "" ...@@ -1751,9 +1833,6 @@ msgstr ""
msgid "Edit files in the editor and commit changes here" msgid "Edit files in the editor and commit changes here"
msgstr "" msgstr ""
msgid "Editing"
msgstr ""
msgid "Email" msgid "Email"
msgstr "" msgstr ""
...@@ -1793,6 +1872,9 @@ msgstr "" ...@@ -1793,6 +1872,9 @@ msgstr ""
msgid "Enable the Performance Bar for a given group." msgid "Enable the Performance Bar for a given group."
msgstr "" msgstr ""
msgid "Environments"
msgstr ""
msgid "Environments|An error occurred while fetching the environments." msgid "Environments|An error occurred while fetching the environments."
msgstr "" msgstr ""
...@@ -2005,6 +2087,9 @@ msgstr "" ...@@ -2005,6 +2087,9 @@ msgstr ""
msgid "GPG Keys" msgid "GPG Keys"
msgstr "" msgstr ""
msgid "General"
msgstr ""
msgid "Generate a default set of labels" msgid "Generate a default set of labels"
msgstr "" msgstr ""
...@@ -2053,6 +2138,9 @@ msgstr "" ...@@ -2053,6 +2138,9 @@ msgstr ""
msgid "Got it!" msgid "Got it!"
msgstr "" msgstr ""
msgid "Graph"
msgstr ""
msgid "Group CI/CD settings" msgid "Group CI/CD settings"
msgstr "" msgstr ""
...@@ -2166,6 +2254,18 @@ msgstr "" ...@@ -2166,6 +2254,18 @@ msgstr ""
msgid "Housekeeping successfully started" msgid "Housekeeping successfully started"
msgstr "" 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." msgid "If you already have files you can push them using the %{link_to_cli} below."
msgstr "" msgstr ""
...@@ -2199,6 +2299,9 @@ msgstr "" ...@@ -2199,6 +2299,9 @@ msgstr ""
msgid "Instance does not support multiple Kubernetes clusters" msgid "Instance does not support multiple Kubernetes clusters"
msgstr "" msgstr ""
msgid "Integrations"
msgstr ""
msgid "Interested parties can even contribute by pushing commits if they want to." msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr "" msgstr ""
...@@ -2336,6 +2439,9 @@ msgstr "" ...@@ -2336,6 +2439,9 @@ msgstr ""
msgid "LastPushEvent|at" msgid "LastPushEvent|at"
msgstr "" msgstr ""
msgid "Latest changes"
msgstr ""
msgid "Learn more" msgid "Learn more"
msgstr "" msgstr ""
...@@ -2360,6 +2466,9 @@ msgstr "" ...@@ -2360,6 +2466,9 @@ msgstr ""
msgid "Leave project" msgid "Leave project"
msgstr "" msgstr ""
msgid "List"
msgstr ""
msgid "List your GitHub repositories" msgid "List your GitHub repositories"
msgstr "" msgstr ""
...@@ -2462,6 +2571,9 @@ msgstr "" ...@@ -2462,6 +2571,9 @@ msgstr ""
msgid "Milestone" msgid "Milestone"
msgstr "" msgstr ""
msgid "Milestones"
msgstr ""
msgid "Milestones|Delete milestone" msgid "Milestones|Delete milestone"
msgstr "" msgstr ""
...@@ -2719,6 +2831,9 @@ msgstr "" ...@@ -2719,6 +2831,9 @@ msgstr ""
msgid "Opens in a new window" msgid "Opens in a new window"
msgstr "" msgstr ""
msgid "Operations"
msgstr ""
msgid "Options" msgid "Options"
msgstr "" msgstr ""
...@@ -2878,25 +2993,22 @@ msgstr "" ...@@ -2878,25 +2993,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines." msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr "" msgstr ""
msgid "Pipeline|Existing branch name, tag" msgid "Pipeline|Create for"
msgstr "" msgstr ""
msgid "Pipeline|Retry pipeline" msgid "Pipeline|Create pipeline"
msgstr "" msgstr ""
msgid "Pipeline|Retry pipeline #%{pipelineId}?" msgid "Pipeline|Existing branch name or tag"
msgstr "" msgstr ""
msgid "Pipeline|Run Pipeline" msgid "Pipeline|Run Pipeline"
msgstr "" msgstr ""
msgid "Pipeline|Run on" msgid "Pipeline|Search branches"
msgstr ""
msgid "Pipeline|Run pipeline"
msgstr "" 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 "" msgstr ""
msgid "Pipeline|Stop pipeline" msgid "Pipeline|Stop pipeline"
...@@ -2905,7 +3017,7 @@ msgstr "" ...@@ -2905,7 +3017,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?" msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr "" msgstr ""
msgid "Pipeline|You’re about to retry pipeline %{pipelineId}." msgid "Pipeline|Variables"
msgstr "" msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}." msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
...@@ -3022,6 +3134,9 @@ msgstr "" ...@@ -3022,6 +3134,9 @@ msgstr ""
msgid "Progress" msgid "Progress"
msgstr "" msgstr ""
msgid "Project"
msgstr ""
msgid "Project '%{project_name}' is in the process of being deleted." msgid "Project '%{project_name}' is in the process of being deleted."
msgstr "" msgstr ""
...@@ -3073,9 +3188,6 @@ msgstr "" ...@@ -3073,9 +3188,6 @@ msgstr ""
msgid "ProjectLifecycle|Stage" msgid "ProjectLifecycle|Stage"
msgstr "" msgstr ""
msgid "ProjectNetworkGraph|Graph"
msgstr ""
msgid "Projects" msgid "Projects"
msgstr "" msgstr ""
...@@ -3226,6 +3338,9 @@ msgstr "" ...@@ -3226,6 +3338,9 @@ msgstr ""
msgid "Register and see your runners for this group." msgid "Register and see your runners for this group."
msgstr "" msgstr ""
msgid "Registry"
msgstr ""
msgid "Related Commits" msgid "Related Commits"
msgstr "" msgstr ""
...@@ -3268,6 +3383,9 @@ msgstr "" ...@@ -3268,6 +3383,9 @@ msgstr ""
msgid "Repository maintenance" msgid "Repository maintenance"
msgstr "" msgstr ""
msgid "Repository mirror settings"
msgstr ""
msgid "Repository storage" msgid "Repository storage"
msgstr "" msgstr ""
...@@ -3363,6 +3481,9 @@ msgstr "" ...@@ -3363,6 +3481,9 @@ msgstr ""
msgid "Search" msgid "Search"
msgstr "" msgstr ""
msgid "Search branches"
msgstr ""
msgid "Search branches and tags" msgid "Search branches and tags"
msgstr "" msgstr ""
...@@ -3405,6 +3526,9 @@ msgstr "" ...@@ -3405,6 +3526,9 @@ msgstr ""
msgid "Select branch/tag" msgid "Select branch/tag"
msgstr "" msgstr ""
msgid "Select source branch"
msgstr ""
msgid "Select target branch" msgid "Select target branch"
msgstr "" msgstr ""
...@@ -3937,6 +4061,9 @@ msgstr "" ...@@ -3937,6 +4061,9 @@ msgstr ""
msgid "This merge request is locked." msgid "This merge request is locked."
msgstr "" 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." msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr "" msgstr ""
...@@ -4253,9 +4380,6 @@ msgstr "" ...@@ -4253,9 +4380,6 @@ msgstr ""
msgid "Verified" msgid "Verified"
msgstr "" msgstr ""
msgid "View and edit lines"
msgstr ""
msgid "View file @ " msgid "View file @ "
msgstr "" msgstr ""
...@@ -4373,6 +4497,12 @@ msgstr "" ...@@ -4373,6 +4497,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?" msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr "" 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." 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 "" msgstr ""
......
#!/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
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
"required": [ "required": [
"id", "id",
"color", "color",
"text_color",
"description", "description",
"title", "title",
"priority" "priority"
...@@ -29,6 +30,7 @@ ...@@ -29,6 +30,7 @@
}, },
"description": { "type": ["string", "null"] }, "description": { "type": ["string", "null"] },
"title": { "type": "string" }, "title": { "type": "string" },
"title": { "text_color": "string" },
"priority": { "type": ["integer", "null"] } "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 ...@@ -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 }
......
...@@ -180,11 +180,11 @@ describe Gitlab::Metrics::WebTransaction do ...@@ -180,11 +180,11 @@ describe Gitlab::Metrics::WebTransaction do
end end
context 'when request goes to ActionController' do context 'when request goes to ActionController' do
let(:request_format) { :html } let(:request) { double(:request, format: double(:format, ref: :html)) }
before do before do
klass = double(:klass, name: 'TestController') 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 env['action_controller.instance'] = controller
end end
...@@ -195,7 +195,7 @@ describe Gitlab::Metrics::WebTransaction do ...@@ -195,7 +195,7 @@ describe Gitlab::Metrics::WebTransaction do
end end
context 'when the response content type is not :html' do 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 it 'appends the mime type to the transaction action' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' }) expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' })
......
...@@ -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 subject { described_class.assignable_for(project) }
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) }
it { is_expected.to be_empty }
end
context 'does give a specific runner' do it { is_expected.to be_empty }
subject { described_class.assignable_for(another_project) }
it { is_expected.to contain_exactly(runner) }
end
end end
context 'with locked runner' do context 'with a different project' do
before do subject { described_class.assignable_for(another_project) }
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 } it { is_expected.to include(unlocked_project_runner) }
end 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
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
......
...@@ -565,4 +565,10 @@ describe CommitStatus do ...@@ -565,4 +565,10 @@ describe CommitStatus do
it_behaves_like 'commit status enqueued' it_behaves_like 'commit status enqueued'
end end
end end
describe '#present' do
subject { commit_status.present }
it { is_expected.to be_a(CommitStatusPresenter) }
end
end end
...@@ -78,4 +78,10 @@ describe GenericCommitStatus do ...@@ -78,4 +78,10 @@ describe GenericCommitStatus do
it { is_expected.not_to be_nil } it { is_expected.not_to be_nil }
end end
end end
describe '#present' do
subject { generic_commit_status.present }
it { is_expected.to be_a(GenericCommitStatusPresenter) }
end
end end
...@@ -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
...@@ -990,65 +990,25 @@ describe Repository do ...@@ -990,65 +990,25 @@ describe Repository do
subject { repository.add_branch(user, branch_name, target) } subject { repository.add_branch(user, branch_name, target) }
context 'with Gitaly enabled' do it "calls Gitaly's OperationService" do
it "calls Gitaly's OperationService" do expect_any_instance_of(Gitlab::GitalyClient::OperationService)
expect_any_instance_of(Gitlab::GitalyClient::OperationService) .to receive(:user_create_branch).with(branch_name, user, target)
.to receive(:user_create_branch).with(branch_name, user, target) .and_return(nil)
.and_return(nil)
subject
end
it 'creates_the_branch' do
expect(subject.name).to eq(branch_name)
expect(repository.find_branch(branch_name)).not_to be_nil
end
context 'with a non-existing target' do
let(:target) { 'fake-target' }
it "returns false and doesn't create the branch" do subject
expect(subject).to be(false)
expect(repository.find_branch(branch_name)).to be_nil
end
end
end end
context 'with Gitaly disabled', :disable_gitaly do it 'creates_the_branch' do
context 'when pre hooks were successful' do expect(subject.name).to eq(branch_name)
it 'runs without errors' do expect(repository.find_branch(branch_name)).not_to be_nil
hook = double(trigger: [true, nil]) end
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 context 'with a non-existing target' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) let(:target) { 'fake-target' }
expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError) it "returns false and doesn't create the branch" do
expect(repository.find_branch(branch_name)).to be_nil expect(subject).to be(false)
end expect(repository.find_branch(branch_name)).to be_nil
end end
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 it 'loads' do
expect(user.ci_authorized_runners).to contain_exactly(runner) 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_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
...@@ -10,7 +10,7 @@ describe Ci::BuildPresenter do ...@@ -10,7 +10,7 @@ describe Ci::BuildPresenter do
end end
it 'inherits from Gitlab::View::Presenter::Delegated' do 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 end
describe '#initialize' do 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 ...@@ -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
...@@ -36,16 +36,17 @@ describe 'layouts/nav/sidebar/_project' do ...@@ -36,16 +36,17 @@ describe 'layouts/nav/sidebar/_project' do
expect(rendered).to have_text 'Registry' expect(rendered).to have_text 'Registry'
end end
it 'highlights only one tab' do it 'highlights sidebar item and flyout' do
render 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 end
it 'highlights container registry tab only' do it 'highlights container registry tab' do
render 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 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