Commit 9392ff6b authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-staged-changes

parents 5eb7ed75 ffde69d7
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner
retry: 1
......
......@@ -82,7 +82,7 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
gem 'gitlab-gollum-lib', '~> 4.2'
gem 'gitlab-gollum-lib', '~> 4.2', require: false
gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false
......@@ -415,7 +415,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
......@@ -434,5 +434,3 @@ gem 'grape_logging', '~> 1.7'
# Asset synchronization
gem 'asset_sync', '~> 2.2.0'
gem 'goldiloader', '~> 2.0'
......@@ -290,9 +290,9 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.94.0)
gitaly-proto (0.97.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
......@@ -331,9 +331,6 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
goldiloader (2.0.1)
activerecord (>= 4.2, < 5.2)
activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
gon (6.1.0)
......@@ -1064,7 +1061,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.94.0)
gitaly-proto (~> 0.97.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
......@@ -1072,7 +1069,6 @@ DEPENDENCIES
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
goldiloader (~> 2.0)
gon (~> 6.1.0)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
......
......@@ -30,10 +30,10 @@ export default class IssuableContext {
const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
$block.find('.value').show();
$block.find('.value:not(.dont-hide)').show();
} else {
$selectbox.show();
$block.find('.value').hide();
$block.find('.value:not(.dont-hide)').hide();
}
if ($selectbox.is(':visible')) {
......
<script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import callout from '../../vue_shared/components/callout.vue';
export default {
name: 'JobHeaderSection',
components: {
ciHeader,
loadingIcon,
export default {
name: 'JobHeaderSection',
components: {
ciHeader,
loadingIcon,
callout,
},
props: {
job: {
type: Object,
required: true,
},
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
data() {
return {
actions: this.getActions(),
};
},
data() {
return {
actions: this.getActions(),
};
},
computed: {
status() {
return this.job && this.job.status;
},
computed: {
status() {
return this.job && this.job.status;
},
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length;
},
/**
* When job has not started the key will be `false`
* When job started the key will be a string with a date.
*/
jobStarted() {
return !this.job.started === false;
},
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length;
},
watch: {
job() {
this.actions = this.getActions();
},
shouldRenderReason() {
return !!(this.job.status && this.job.callout_message);
},
methods: {
getActions() {
const actions = [];
/**
* When job has not started the key will be `false`
* When job started the key will be a string with a date.
*/
jobStarted() {
return !this.job.started === false;
},
},
watch: {
job() {
this.actions = this.getActions();
},
},
methods: {
getActions() {
const actions = [];
if (this.job.new_issue_path) {
actions.push({
label: 'New issue',
path: this.job.new_issue_path,
cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
type: 'link',
});
}
return actions;
},
if (this.job.new_issue_path) {
actions.push({
label: 'New issue',
path: this.job.new_issue_path,
cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
type: 'link',
});
}
return actions;
},
};
},
};
</script>
<template>
<div class="js-build-header build-header top-area">
<ci-header
v-if="shouldRenderContent"
:status="status"
item-name="Job"
:item-id="job.id"
:time="job.created_at"
:user="job.user"
:actions="actions"
:has-sidebar-button="true"
:should-render-triggered-label="jobStarted"
/>
<loading-icon
v-if="isLoading"
size="2"
class="prepend-top-default append-bottom-default"
<header>
<div class="js-build-header build-header top-area">
<ci-header
v-if="shouldRenderContent"
:status="status"
item-name="Job"
:item-id="job.id"
:time="job.created_at"
:user="job.user"
:actions="actions"
:has-sidebar-button="true"
:should-render-triggered-label="jobStarted"
/>
<loading-icon
v-if="isLoading"
size="2"
class="prepend-top-default append-bottom-default"
/>
</div>
<callout
v-if="shouldRenderReason"
:message="job.callout_message"
/>
</div>
</header>
</template>
<script>
import detailRow from './sidebar_detail_row.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
import detailRow from './sidebar_detail_row.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
export default {
name: 'SidebarDetailsBlock',
components: {
detailRow,
loadingIcon,
export default {
name: 'SidebarDetailsBlock',
components: {
detailRow,
loadingIcon,
},
mixins: [timeagoMixin],
props: {
job: {
type: Object,
required: true,
},
mixins: [
timeagoMixin,
],
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
isLoading: {
type: Boolean,
required: true,
},
computed: {
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length > 0;
},
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `#${this.job.runner.id}`;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
return '';
}
canUserRetry: {
type: Boolean,
required: false,
default: false,
},
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length > 0;
},
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `#${this.job.runner.id}`;
},
retryButtonClass() {
let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
className +=
this.job.status && this.job.recoverable
? ' btn-primary'
: ' btn-inverted-secondary';
return className;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
return '';
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`;
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`;
}
return t;
},
renderBlock() {
return this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path;
},
return t;
},
};
renderBlock() {
return (
this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path
);
},
},
};
</script>
<template>
<div>
<div class="block">
<strong class="inline prepend-top-8">
{{ job.name }}
</strong>
<a
v-if="canUserRetry"
:class="retryButtonClass"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
{{ __('Retry') }}
</a>
<button
type="button"
:aria-label="__('Toggle Sidebar')"
class="btn btn-blank gutter-toggle pull-right
visible-xs-block visible-sm-block js-sidebar-build-toggle"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-angle-double-right"
></i>
</button>
</div>
<template v-if="shouldRenderContent">
<div
class="block retry-link"
......@@ -85,16 +124,16 @@
class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path"
>
New issue
{{ __('New issue') }}
</a>
<a
v-if="job.retry_path"
v-if="canUserRetry"
class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
Retry
{{ __('Retry') }}
</a>
</div>
<div :class="{block : renderBlock }">
......@@ -103,7 +142,7 @@
v-if="job.merge_request"
>
<span class="build-light-text">
Merge Request:
{{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
......@@ -158,7 +197,7 @@
v-if="job.tags.length"
>
<span class="build-light-text">
Tags:
{{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
......@@ -178,7 +217,7 @@
data-method="post"
rel="nofollow"
>
Cancel
{{ __('Cancel') }}
</a>
</div>
</div>
......
......@@ -35,9 +35,11 @@ export default () => {
});
// Sidebar information block
const detailsBlockElement = document.getElementById('js-details-block-vue');
const detailsBlockDataset = detailsBlockElement.dataset;
// eslint-disable-next-line
new Vue({
el: '#js-details-block-vue',
el: detailsBlockElement,
components: {
detailsBlock,
},
......@@ -50,6 +52,7 @@ export default () => {
return createElement('details-block', {
props: {
isLoading: this.mediator.state.isLoading,
canUserRetry: !!('canUserRetry' in detailsBlockDataset),
job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
},
......
<script>
const calloutVariants = ['danger', 'success', 'info', 'warning'];
export default {
props: {
category: {
type: String,
required: false,
default: calloutVariants[0],
validator: value => calloutVariants.includes(value),
},
message: {
type: String,
required: true,
},
},
};
</script>
<template>
<div
:class="`bs-callout bs-callout-${category}`"
role="alert"
aria-live="assertive"
>
{{ message }}
</div>
</template>
......@@ -16,7 +16,7 @@
.nav-header-btn {
padding: 10px $gl-sidebar-padding;
color: inherit;
transition-duration: .3s;
transition-duration: 0.3s;
position: absolute;
top: 0;
cursor: pointer;
......@@ -137,6 +137,12 @@
}
}
.issuable-sidebar .labels {
.value.dont-hide ~ .selectbox {
padding-top: $gl-padding-8;
}
}
.pikaday-container {
.pika-single {
margin-top: 2px;
......@@ -151,4 +157,3 @@
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
@keyframes fade-out-status {
0%, 50% { opacity: 1; }
100% { opacity: 0; }
0%,
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes blinking-dots {
0% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
24px 0 0 0 rgba($white-light, 0.2);
24px 0 0 0 rgba($white-light, 0.2);
}
25% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 2),
24px 0 0 0 rgba($white-light, 0.2);
24px 0 0 0 rgba($white-light, 0.2);
}
75% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
24px 0 0 0 rgba($white-light, 1);
24px 0 0 0 rgba($white-light, 1);
}
100% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
24px 0 0 0 rgba($white-light, 0.2);
24px 0 0 0 rgba($white-light, 0.2);
}
}
@keyframes blinking-scroll-button {
0% { opacity: 0.2; }
25% { opacity: 0.5; }
50% { opacity: 0.7; }
100% { opacity: 1; }
0% {
opacity: 0.2;
}
25% {
opacity: 0.5;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
.build-page {
......@@ -125,12 +142,12 @@
.btn-scroll.animate {
.first-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .3s;
animation-delay: 0.3s;
}
.second-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .2s;
animation-delay: 0.2s;
}
.third-triangle {
......
......@@ -78,6 +78,8 @@ class Projects::JobsController < Projects::ApplicationController
result.merge!(trace.to_h)
end
result[:html] = result[:html].presence || 'No job log'
render json: result
end
end
......
......@@ -29,8 +29,7 @@ class Projects::WikisController < Projects::ApplicationController
else
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = WikiPage.new(@project_wiki)
@page.title = params[:id]
@page = build_page(title: params[:id])
render 'edit'
end
......@@ -54,7 +53,7 @@ class Projects::WikisController < Projects::ApplicationController
else
render 'edit'
end
rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e
@error = e
render 'edit'
end
......@@ -70,6 +69,11 @@ class Projects::WikisController < Projects::ApplicationController
else
render action: "edit"
end
rescue Gitlab::Git::Wiki::OperationError => e
@page = build_page(wiki_params)
@error = e
render 'edit'
end
def history
......@@ -94,6 +98,9 @@ class Projects::WikisController < Projects::ApplicationController
redirect_to project_wiki_path(@project, :home),
status: 302,
notice: "Page was successfully deleted"
rescue Gitlab::Git::Wiki::OperationError => e
@error = e
render 'edit'
end
def git_access
......@@ -116,4 +123,10 @@ class Projects::WikisController < Projects::ApplicationController
def wiki_params
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
def build_page(args)
WikiPage.new(@project_wiki).tap do |page|
page.update_attributes(args)
end
end
end
......@@ -134,10 +134,8 @@ class GroupDescendantsFinder
end
def direct_child_projects
GroupProjectsFinder.new(group: parent_group,
current_user: current_user,
options: { only_owned: true },
params: params).execute
GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params)
.execute
end
# Finds all projects nested under `parent_group` or any of its descendant
......
......@@ -28,7 +28,7 @@ module NavHelper
end
elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded]
elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access')
elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access', 'destroy')
%w[page-gutter wiki-sidebar right-sidebar-expanded]
else
[]
......
......@@ -611,7 +611,7 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
variables.append(key: 'GITLAB_CI', value: 'true')
variables.append(key: 'GITLAB_FEATURES', value: project.namespace.features.join(','))
variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(','))
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
......
......@@ -13,7 +13,7 @@ module Ci
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, -> { auto_include(false) }, through: :runner_projects
has_many :projects, through: :runner_projects
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
......
......@@ -15,7 +15,7 @@ module Clusters
belongs_to :user
has_many :cluster_projects, class_name: 'Clusters::Project'
has_many :projects, -> { auto_include(false) }, through: :cluster_projects, class_name: '::Project'
has_many :projects, through: :cluster_projects, class_name: '::Project'
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
......
......@@ -37,7 +37,20 @@ module GroupDescendant
parent ||= preloaded.detect { |possible_parent| possible_parent.is_a?(Group) && possible_parent.id == child.parent_id }
if parent.nil? && !child.parent_id.nil?
raise ArgumentError.new('parent was not preloaded')
parent = child.parent
exception = ArgumentError.new <<~MSG
parent: [GroupDescendant: #{parent.inspect}] was not preloaded for [#{child.inspect}]")
This error is not user facing, but causes a +1 query.
MSG
extras = {
parent: parent,
child: child,
preloaded: preloaded.map(&:full_path)
}
issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/40785'
Gitlab::Sentry.track_exception(exception, issue_url: issue_url, extra: extras)
end
if parent.nil? && hierarchy_top.present?
......
......@@ -48,7 +48,7 @@ module Issuable
end
has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, -> { auto_include(false) }, through: :label_links
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :metrics
......
......@@ -102,7 +102,7 @@ module ResolvableDiscussion
yield(notes_relation)
# Set the notes array to the updated notes
@notes = notes_relation.fresh.auto_include(false).to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
@notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
self.class.memoized_values.each do |name|
clear_memoization(name)
......
......@@ -2,7 +2,7 @@ class DeployKey < Key
include IgnorableColumn
has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, -> { auto_include(false) }, through: :deploy_keys_projects
has_many :projects, through: :deploy_keys_projects
scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) }
scope :are_public, -> { where(public: true) }
......
......@@ -8,7 +8,7 @@ class DeployToken < ActiveRecord::Base
default_value_for(:expires_at) { Forever.date }
has_many :project_deploy_tokens, inverse_of: :deploy_token
has_many :projects, -> { auto_include(false) }, through: :project_deploy_tokens
has_many :projects, through: :project_deploy_tokens
validate :ensure_at_least_one_scope
before_save :ensure_token
......
class ForkNetwork < ActiveRecord::Base
belongs_to :root_project, class_name: 'Project'
has_many :fork_network_members
has_many :projects, -> { auto_include(false) }, through: :fork_network_members
has_many :projects, through: :fork_network_members
after_create :add_root_as_member, if: :root_project
......
......@@ -12,9 +12,9 @@ class Group < Namespace
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
has_many :users, -> { auto_include(false) }, through: :group_members
has_many :users, through: :group_members
has_many :owners,
-> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) },
-> { where(members: { access_level: Gitlab::Access::OWNER }) },
through: :group_members,
source: :user
......@@ -23,7 +23,7 @@ class Group < Namespace
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, -> { auto_include(false) }, through: :project_group_links, source: :project
has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
......
......@@ -34,7 +34,7 @@ class Issue < ActiveRecord::Base
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
has_many :assignees, -> { auto_include(false) }, class_name: "User", through: :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
validates :project, presence: true
......
......@@ -18,8 +18,8 @@ class Label < ActiveRecord::Base
has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :issues, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'MergeRequest'
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
before_validation :strip_whitespace_from_title_and_color
......
......@@ -3,7 +3,7 @@ class LfsObject < ActiveRecord::Base
include ObjectStorage::BackgroundMove
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, -> { auto_include(false) }, through: :lfs_objects_projects
has_many :projects, through: :lfs_objects_projects
scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
......
......@@ -22,7 +22,7 @@ class Milestone < ActiveRecord::Base
belongs_to :group
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title').auto_include(false) }, through: :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......
......@@ -248,10 +248,6 @@ class Namespace < ActiveRecord::Base
all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
end
def features
[]
end
def refresh_project_authorizations
owner.refresh_authorized_projects
end
......
......@@ -138,11 +138,11 @@ class Project < ActiveRecord::Base
has_one :packagist_service
# TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id"
has_one :forked_from_project, -> { auto_include(false) }, through: :forked_project_link
has_one :forked_project_link, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id"
has_many :forks, -> { auto_include(false) }, through: :forked_project_links, source: :forked_to_project
has_many :forks, through: :forked_project_links, source: :forked_to_project
# TODO: replace these relations with the fork network versions
has_one :root_of_fork_network,
......@@ -150,7 +150,7 @@ class Project < ActiveRecord::Base
inverse_of: :root_project,
class_name: 'ForkNetwork'
has_one :fork_network_member
has_one :fork_network, -> { auto_include(false) }, through: :fork_network_member
has_one :fork_network, through: :fork_network_member
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
......@@ -167,27 +167,27 @@ class Project < ActiveRecord::Base
has_many :protected_tags
has_many :project_authorizations
has_many :authorized_users, -> { auto_include(false) }, through: :project_authorizations, source: :user, class_name: 'User'
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) },
as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :project_members
has_many :users, -> { auto_include(false) }, through: :project_members
has_many :users, through: :project_members
has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
has_many :deploy_keys_projects
has_many :deploy_keys, -> { auto_include(false) }, through: :deploy_keys_projects
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects
has_many :starrers, -> { auto_include(false) }, through: :users_star_projects, source: :user
has_many :starrers, through: :users_star_projects, source: :user
has_many :releases
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :lfs_objects, -> { auto_include(false) }, through: :lfs_objects_projects
has_many :lfs_objects, through: :lfs_objects_projects
has_many :lfs_file_locks
has_many :project_group_links
has_many :invited_groups, -> { auto_include(false) }, through: :project_group_links, source: :group
has_many :invited_groups, through: :project_group_links, source: :group
has_many :pages_domains
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
......@@ -199,7 +199,7 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project'
has_many :clusters, -> { auto_include(false) }, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
......@@ -216,16 +216,16 @@ class Project < ActiveRecord::Base
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :runner_projects, class_name: 'Ci::RunnerProject'
has_many :runners, -> { auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
has_many :deploy_tokens, -> { auto_include(false) }, through: :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens
has_many :active_runners, -> { active.auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
......@@ -1875,6 +1875,10 @@ class Project < ActiveRecord::Base
memoized_results[cache_key]
end
def licensed_features
[]
end
private
def storage
......
......@@ -179,7 +179,11 @@ class ProjectWiki
def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title)
Gitlab::Git::Wiki::CommitDetails.new(@user.name, @user.email, commit_message)
Gitlab::Git::Wiki::CommitDetails.new(@user.id,
@user.username,
@user.name,
@user.email,
commit_message)
end
def default_message(action, title)
......
......@@ -22,7 +22,7 @@ class Todo < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
belongs_to :target, -> { auto_include(false) }, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true
......
......@@ -96,23 +96,23 @@ class User < ActiveRecord::Base
# Groups
has_many :members
has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
has_many :groups, -> { auto_include(false) }, through: :group_members
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) }, through: :group_members, source: :group
has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }).auto_include(false) }, through: :group_members, source: :group
has_many :groups, through: :group_members
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }) }, through: :group_members, source: :group
# Projects
has_many :groups_projects, -> { auto_include(false) }, through: :groups, source: :projects
has_many :personal_projects, -> { auto_include(false) }, through: :namespace, source: :projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, -> { where(requested_at: nil) }
has_many :projects, -> { auto_include(false) }, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :starred_projects, -> { auto_include(false) }, through: :users_star_projects, source: :project
has_many :starred_projects, through: :users_star_projects, source: :project
has_many :project_authorizations
has_many :authorized_projects, -> { auto_include(false) }, through: :project_authorizations, source: :project
has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :user_interacted_projects
has_many :project_interactions, -> { auto_include(false) }, through: :user_interacted_projects, source: :project, class_name: 'Project'
has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project'
has_many :snippets, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
......@@ -132,7 +132,7 @@ class User < ActiveRecord::Base
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
has_many :assigned_issues, -> { auto_include(false) }, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
......
......@@ -265,6 +265,15 @@ class WikiPage
title.present? && self.class.unhyphenize(@page.url_path) != title
end
# Updates the current @attributes hash by merging a hash of params
def update_attributes(attrs)
attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
attrs.slice!(:content, :format, :message, :title)
@attributes.merge!(attrs)
end
private
# Process and format the title based on the user input.
......@@ -290,15 +299,6 @@ class WikiPage
File.join(components)
end
# Updates the current @attributes hash by merging a hash of params
def update_attributes(attrs)
attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
attrs.slice!(:content, :format, :message, :title)
@attributes.merge!(attrs)
end
def set_attributes
attributes[:slug] = @page.url_path
attributes[:title] = @page.title
......
module Ci
class BuildPresenter < Gitlab::View::Presenter::Delegated
CALLOUT_FAILURE_MESSAGES = {
unknown_failure: 'There is an unknown failure, please try again',
script_failure: 'There has been a script failure. Check the job log for more information',
api_failure: 'There has been an API failure, please try again',
stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
runner_system_failure: 'There has been a runner system failure, please try again',
missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
}.freeze
presents :build
def erased_by_user?
......@@ -35,6 +44,14 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}"
end
def callout_failure_message
CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
end
def recoverable?
failed? && !unrecoverable?
end
private
def tooltip_for_badge
......@@ -44,5 +61,9 @@ module Ci
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
def unrecoverable?
script_failure? || missing_dependency_failure?
end
end
end
......@@ -26,6 +26,8 @@ class JobEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
expose :callout_message, if: -> (*) { failed? }
expose :recoverable, if: -> (*) { failed? }
private
......@@ -50,4 +52,20 @@ class JobEntity < Grape::Entity
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend
end
def failed?
build.failed?
end
def callout_message
build_presenter.callout_failure_message
end
def recoverable
build_presenter.recoverable?
end
def build_presenter
@build_presenter ||= build.present
end
end
......@@ -33,7 +33,7 @@ module Ci
end
end
builds.auto_include(false).find do |build|
builds.find do |build|
next unless runner.can_pick?(build)
begin
......
<%= yield -%>
---
-- <%# signature marker %>
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
<%= yield -%>
---
-- <%# signature marker %>
<% if @target_url -%>
<% if @reply_by_email -%>
<%= "Reply to this email directly or view it on GitLab: #{@target_url}" -%>
......
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
.block
%strong.inline.prepend-top-8
= @build.name
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post
%a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' }
= icon('angle-double-right')
#js-details-block-vue
#js-details-block-vue{ data: { can_user_retry: can?(current_user, :update_build, @build) && @build.retryable? } }
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block
......
......@@ -4,7 +4,7 @@
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value.issuable-show-labels
.value.issuable-show-labels.dont-hide
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
None
%a{ href: "#",
......
......@@ -96,7 +96,7 @@
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
.value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label|
= link_to_label(label, subject: issuable.project, type: issuable.to_ability_name)
......
---
title: Keep current labels visible when editing them in the sidebar
merge_request:
author:
type: changed
---
title: Use RFC 3676 mail signature delimiters
merge_request: 17979
author: Enrico Scholz
type: changed
---
title: Show shared projects on group page
merge_request: 18390
author:
type: fixed
---
title: Add Goldiloader to fix N+1 issues when calculating email recipients
merge_request:
author:
type: performance
---
title: Triggering custom hooks by Wiki UI edit
merge_request: 18251
author:
type: fixed
......@@ -17,7 +17,11 @@ codequality:
- docker:stable-dind
script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
- docker run
--env SOURCE_CODE="$PWD"
--volume "$PWD":/code
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts:
paths: [codeclimate.json]
```
......
......@@ -42,9 +42,9 @@ dast:
allow_failure: true
script:
- mkdir /zap/wrk/
- /zap/zap-baseline.py -J gl-dast-report.json -t $website \
--auth-url $login_url \
--auth-username "john.doe@example.com" \
- /zap/zap-baseline.py -J gl-dast-report.json -t $website
--auth-url $login_url
--auth-username "john.doe@example.com"
--auth-password "john-doe-password" || true
- cp /zap/wrk/gl-dast-report.json .
artifacts:
......
......@@ -75,7 +75,7 @@ cancel the job, retry it, or erase the job trace.
## Seeing the failure reason for jobs
> [Introduced][ce-5742] in GitLab 10.7.
> [Introduced][ce-17782] in GitLab 10.7.
When a pipeline fails or is allowed to fail, there are several places where you
can quickly check the reason it failed:
......@@ -88,6 +88,8 @@ In any case, if you hover over the failed job you can see the reason it failed.
![Pipeline detail](img/job_failure_reason.png)
From [GitLab 10.8][ce-17814] you can also see the reason it failed on the Job detail page.
## Pipeline graphs
> [Introduced][ce-5742] in GitLab 8.11.
......@@ -279,4 +281,5 @@ runners will not use regular runners, they must be tagged accordingly.
[ce-7931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931
[ce-9760]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760
[ce-17782]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782
[ce-17814]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17814
[regexp]: https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99
......@@ -133,11 +133,19 @@ roughly be as follows:
1. Release B:
1. Deploy code so that the application starts using the new column and stops
scheduling jobs for newly created data.
1. In a post-deployment migration you'll need to ensure no jobs remain. To do
so you can use `Gitlab::BackgroundMigration.steal` to process any remaining
jobs before continuing.
1. In a post-deployment migration you'll need to ensure no jobs remain.
1. Use `Gitlab::BackgroundMigration.steal` to process any remaining
jobs in Sidekiq.
1. Reschedule the migration to be run directly (i.e. not through Sidekiq)
on any rows that weren't migrated by Sidekiq. This can happen if, for
instance, Sidekiq received a SIGKILL, or if a particular batch failed
enough times to be marked as dead.
1. Remove the old column.
This may also require a bump to the [import/export version][import-export], if
importing a project from a prior version of GitLab requires the data to be in
the new format.
## Example
To explain all this, let's use the following example: the table `services` has a
......@@ -296,3 +304,4 @@ for more details.
[migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md
[issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351
[reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
[import-export]: ../user/project/settings/import_export.md
......@@ -2,10 +2,11 @@ module Gitlab
module Git
class Wiki
DuplicatePageError = Class.new(StandardError)
OperationError = Class.new(StandardError)
CommitDetails = Struct.new(:name, :email, :message) do
CommitDetails = Struct.new(:user_id, :username, :name, :email, :message) do
def to_h
{ name: name, email: email, message: message }
{ user_id: user_id, username: username, name: name, email: email, message: message }
end
end
PageBlob = Struct.new(:name)
......@@ -140,6 +141,10 @@ module Gitlab
end
end
def gollum_wiki
@gollum_wiki ||= Gollum::Wiki.new(@repository.path)
end
private
# options:
......@@ -158,10 +163,6 @@ module Gitlab
offset: options[:offset])
end
def gollum_wiki
@gollum_wiki ||= Gollum::Wiki.new(@repository.path)
end
def gollum_page_by_path(page_path)
page_name = Gollum::Page.canonicalize_filename(page_path)
page_dir = File.split(page_path).first
......@@ -201,12 +202,12 @@ module Gitlab
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
filename = File.basename(name)
dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
gollum_wiki.write_page(filename, format, content, commit_details.to_h, dir)
with_committer_with_hooks(commit_details) do |committer|
filename = File.basename(name)
dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
nil
gollum_wiki.write_page(filename, format, content, { committer: committer }, dir)
end
rescue Gollum::DuplicatePageError => e
raise Gitlab::Git::Wiki::DuplicatePageError, e.message
end
......@@ -214,24 +215,23 @@ module Gitlab
def gollum_delete_page(page_path, commit_details)
assert_type!(commit_details, CommitDetails)
gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
nil
with_committer_with_hooks(commit_details) do |committer|
gollum_wiki.delete_page(gollum_page_by_path(page_path), committer: committer)
end
end
def gollum_update_page(page_path, title, format, content, commit_details)
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
page = gollum_page_by_path(page_path)
committer = Gollum::Committer.new(page.wiki, commit_details.to_h)
# Instead of performing two renames if the title has changed,
# the update_page will only update the format and content and
# the rename_page will do anything related to moving/renaming
gollum_wiki.update_page(page, page.name, format, content, committer: committer)
gollum_wiki.rename_page(page, title, committer: committer)
committer.commit
nil
with_committer_with_hooks(commit_details) do |committer|
page = gollum_page_by_path(page_path)
# Instead of performing two renames if the title has changed,
# the update_page will only update the format and content and
# the rename_page will do anything related to moving/renaming
gollum_wiki.update_page(page, page.name, format, content, committer: committer)
gollum_wiki.rename_page(page, title, committer: committer)
end
end
def gollum_find_page(title:, version: nil, dir: nil)
......@@ -288,6 +288,20 @@ module Gitlab
Gitlab::Git::WikiPage.new(wiki_page, version)
end
end
def committer_with_hooks(commit_details)
Gitlab::Wiki::CommitterWithHooks.new(self, commit_details.to_h)
end
def with_committer_with_hooks(commit_details, &block)
committer = committer_with_hooks(commit_details)
yield committer
committer.commit
nil
end
end
end
end
......@@ -200,6 +200,8 @@ module Gitlab
def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new(
user_id: commit_details.user_id,
user_name: encode_binary(commit_details.username),
name: encode_binary(commit_details.name),
email: encode_binary(commit_details.email),
message: encode_binary(commit_details.message)
......
......@@ -2,10 +2,14 @@ module Gitlab
module GlId
def self.gl_id(user)
if user.present?
"user-#{user.id}"
gl_id_from_id_value(user.id)
else
""
''
end
end
def self.gl_id_from_id_value(id)
"user-#{id}"
end
end
end
......@@ -20,6 +20,10 @@ module Gitlab
subject
end
def present(**attributes)
self
end
class_methods do
def presenter?
true
......
module Gitlab
module Wiki
class CommitterWithHooks < Gollum::Committer
attr_reader :gl_wiki
def initialize(gl_wiki, options = {})
@gl_wiki = gl_wiki
super(gl_wiki.gollum_wiki, options)
end
def commit
result = Gitlab::Git::OperationService.new(git_user, gl_wiki.repository).with_branch(
@wiki.ref,
start_branch_name: @wiki.ref
) do |start_commit|
super(false)
end
result[:newrev]
rescue Gitlab::Git::HooksService::PreReceiveError => e
message = "Custom Hook failed: #{e.message}"
raise Gitlab::Git::Wiki::OperationError, message
end
private
def git_user
@git_user ||= Gitlab::Git::User.new(@options[:username],
@options[:name],
@options[:email],
gitlab_id)
end
def gitlab_id
Gitlab::GlId.gl_id_from_id_value(@options[:user_id])
end
end
end
end
require 'gitlab/styles/rubocop/model_helpers'
module RuboCop
module Cop
module Gitlab
class HasManyThroughScope < RuboCop::Cop::Cop
include ::Gitlab::Styles::Rubocop::ModelHelpers
MSG = 'Always provide an explicit scope calling auto_include(false) when using has_many :through'.freeze
def_node_search :through?, <<~PATTERN
(pair (sym :through) _)
PATTERN
def_node_matcher :has_many_through?, <<~PATTERN
(send nil? :has_many ... #through?)
PATTERN
def_node_search :disables_auto_include?, <<~PATTERN
(send _ :auto_include false)
PATTERN
def_node_matcher :scope_disables_auto_include?, <<~PATTERN
(block (send nil? :lambda) _ #disables_auto_include?)
PATTERN
def on_send(node)
return unless in_model?(node)
return unless has_many_through?(node)
target = node
scope_argument = node.children[3]
if scope_argument.children[0].children.last == :lambda
return if scope_disables_auto_include?(scope_argument)
target = scope_argument
end
add_offense(target, location: :expression)
end
end
end
end
end
# rubocop:disable Naming/FileName
require_relative 'cop/gitlab/has_many_through_scope'
require_relative 'cop/gitlab/httparty'
require_relative 'cop/gitlab/module_with_instance_variables'
require_relative 'cop/gitlab/predicate_memoization'
require_relative 'cop/gitlab/httparty'
require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/avoid_return_from_blocks'
require_relative 'cop/avoid_break_from_strong_memoize'
......
......@@ -190,7 +190,10 @@ describe Projects::JobsController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
expect(json_response['html']).to be_nil
end
it 'returns no job log message' do
expect(json_response['html']).to eq('No job log')
end
end
......
......@@ -243,5 +243,10 @@ FactoryBot.define do
failed
failure_reason 1
end
trait :api_failure do
failed
failure_reason 2
end
end
end
......@@ -237,6 +237,22 @@ describe 'Issue Boards', :js do
end
context 'labels' do
it 'shows current labels when editing' do
click_card(card)
page.within('.labels') do
click_link 'Edit'
wait_for_requests
page.within('.value') do
expect(page).to have_selector('.label', count: 2)
expect(page).to have_content(development.title)
expect(page).to have_content(stretch.title)
end
end
end
it 'adds a single label' do
click_card(card)
......@@ -296,7 +312,9 @@ describe 'Issue Boards', :js do
wait_for_requests
click_link stretch.title
within('.dropdown-menu-labels') do
click_link stretch.title
end
wait_for_requests
......
......@@ -5,9 +5,9 @@ feature 'Issue Sidebar' do
let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) }
let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
let!(:label) { create(:label, project: project, title: 'bug') }
let(:issue) { create(:labeled_issue, project: project, labels: [label]) }
let!(:xss_label) { create(:label, project: project, title: '&lt;script&gt;alert("xss");&lt;&#x2F;script&gt;') }
before do
......@@ -112,11 +112,18 @@ feature 'Issue Sidebar' do
context 'editing issue labels', :js do
before do
issue.update_attributes(labels: [label])
page.within('.block.labels') do
find('.edit-link').click
end
end
it 'shows the current set of labels' do
page.within('.issuable-show-labels') do
expect(page).to have_content label.title
end
end
it 'shows option to create a project label' do
page.within('.block.labels') do
expect(page).to have_content 'Create project'
......
......@@ -491,16 +491,18 @@ feature 'Jobs' do
end
end
describe "POST /:project/jobs/:id/retry" do
describe "POST /:project/jobs/:id/retry", :js do
context "Job from project", :js do
before do
job.run!
job.cancel!
visit project_job_path(project, job)
find('.js-cancel-job').click()
wait_for_requests
find('.js-retry-button').click
end
it 'shows the right status and buttons', :js do
it 'shows the right status and buttons' do
page.within('aside.right-sidebar') do
expect(page).to have_content 'Cancel'
end
......
......@@ -35,15 +35,6 @@ describe GroupDescendantsFinder do
expect(finder.execute).to contain_exactly(project)
end
it 'does not include projects shared with the group' do
project = create(:project, namespace: group)
other_project = create(:project)
other_project.project_group_links.create(group: group,
group_access: ProjectGroupLink::MASTER)
expect(finder.execute).to contain_exactly(project)
end
context 'when archived is `true`' do
let(:params) { { archived: 'true' } }
......
......@@ -36,14 +36,28 @@ describe('Job details header', () => {
},
isLoading: false,
};
vm = mountComponent(HeaderComponent, props);
});
afterEach(() => {
vm.$destroy();
});
describe('job reason', () => {
it('should not render the reason when reason is absent', () => {
vm = mountComponent(HeaderComponent, props);
expect(vm.shouldRenderReason).toBe(false);
});
it('should render the reason when reason is present', () => {
props.job.callout_message = 'There is an unknown failure, please try again';
vm = mountComponent(HeaderComponent, props);
expect(vm.shouldRenderReason).toBe(true);
});
});
describe('triggered job', () => {
beforeEach(() => {
vm = mountComponent(HeaderComponent, props);
......@@ -51,14 +65,17 @@ describe('Job details header', () => {
it('should render provided job information', () => {
expect(
vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
vm.$el
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
).toEqual('failed Job #123 triggered 3 weeks ago by Foo');
});
it('should render new issue link', () => {
expect(
vm.$el.querySelector('.js-new-issue').getAttribute('href'),
).toEqual(props.job.new_issue_path);
expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
props.job.new_issue_path,
);
});
});
......@@ -68,7 +85,10 @@ describe('Job details header', () => {
vm = mountComponent(HeaderComponent, props);
expect(
vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
vm.$el
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
).toEqual('failed Job #123 created 3 weeks ago by Foo');
});
});
......
......@@ -31,10 +31,25 @@ describe('Sidebar details block', () => {
});
});
describe("when user can't retry", () => {
it('should not render a retry button', () => {
vm = new SidebarComponent({
propsData: {
job: {},
canUserRetry: false,
isLoading: true,
},
}).$mount();
expect(vm.$el.querySelector('.js-retry-job')).toBeNull();
});
});
beforeEach(() => {
vm = new SidebarComponent({
propsData: {
job,
canUserRetry: true,
isLoading: false,
},
}).$mount();
......@@ -42,7 +57,9 @@ describe('Sidebar details block', () => {
describe('actions', () => {
it('should render link to new issue', () => {
expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(job.new_issue_path);
expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
job.new_issue_path,
);
expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue');
});
......@@ -57,43 +74,35 @@ describe('Sidebar details block', () => {
describe('information', () => {
it('should render merge request link', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-mr')),
).toEqual('Merge Request: !2');
expect(trimWhitespace(vm.$el.querySelector('.js-job-mr'))).toEqual('Merge Request: !2');
expect(
vm.$el.querySelector('.js-job-mr a').getAttribute('href'),
).toEqual(job.merge_request.path);
expect(vm.$el.querySelector('.js-job-mr a').getAttribute('href')).toEqual(
job.merge_request.path,
);
});
it('should render job duration', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-duration')),
).toEqual('Duration: 6 seconds');
expect(trimWhitespace(vm.$el.querySelector('.js-job-duration'))).toEqual(
'Duration: 6 seconds',
);
});
it('should render erased date', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-erased')),
).toEqual('Erased: 3 weeks ago');
expect(trimWhitespace(vm.$el.querySelector('.js-job-erased'))).toEqual('Erased: 3 weeks ago');
});
it('should render finished date', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-finished')),
).toEqual('Finished: 3 weeks ago');
expect(trimWhitespace(vm.$el.querySelector('.js-job-finished'))).toEqual(
'Finished: 3 weeks ago',
);
});
it('should render queued date', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-queued')),
).toEqual('Queued: 9 seconds');
expect(trimWhitespace(vm.$el.querySelector('.js-job-queued'))).toEqual('Queued: 9 seconds');
});
it('should render runner ID', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-runner')),
).toEqual('Runner: #1');
expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: #1');
});
it('should render timeout information', () => {
......@@ -103,15 +112,11 @@ describe('Sidebar details block', () => {
});
it('should render coverage', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-coverage')),
).toEqual('Coverage: 20%');
expect(trimWhitespace(vm.$el.querySelector('.js-job-coverage'))).toEqual('Coverage: 20%');
});
it('should render tags', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-tags')),
).toEqual('Tags: tag');
expect(trimWhitespace(vm.$el.querySelector('.js-job-tags'))).toEqual('Tags: tag');
});
});
});
import Vue from 'vue';
import callout from '~/vue_shared/components/callout.vue';
import createComponent from 'spec/helpers/vue_mount_component_helper';
describe('Callout Component', () => {
let CalloutComponent;
let vm;
const exampleMessage = 'This is a callout message!';
beforeEach(() => {
CalloutComponent = Vue.extend(callout);
});
afterEach(() => {
vm.$destroy();
});
it('should render the appropriate variant of callout', () => {
vm = createComponent(CalloutComponent, {
category: 'info',
message: exampleMessage,
});
expect(vm.$el.getAttribute('class')).toEqual('bs-callout bs-callout-info');
expect(vm.$el.tagName).toEqual('DIV');
});
it('should render accessibility attributes', () => {
vm = createComponent(CalloutComponent, {
message: exampleMessage,
});
expect(vm.$el.getAttribute('role')).toEqual('alert');
expect(vm.$el.getAttribute('aria-live')).toEqual('assertive');
});
it('should render the provided message', () => {
vm = createComponent(CalloutComponent, {
message: exampleMessage,
});
expect(vm.$el.innerHTML.trim()).toEqual(exampleMessage);
});
});
......@@ -30,7 +30,7 @@ describe Gitlab::Git::Wiki do
end
def commit_details(name)
Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "created page #{name}")
Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "created page #{name}")
end
def destroy_page(title, dir = '')
......
......@@ -48,4 +48,11 @@ describe Gitlab::View::Presenter::Base do
end
end
end
describe '#present' do
it 'returns self' do
presenter = presenter_class.new(build_stubbed(:project))
expect(presenter.present).to eq(presenter)
end
end
end
require 'spec_helper'
describe Gitlab::Wiki::CommitterWithHooks, seed_helper: true do
shared_examples 'calling wiki hooks' do
let(:project) { create(:project) }
let(:user) { project.owner }
let(:project_wiki) { ProjectWiki.new(project, user) }
let(:wiki) { project_wiki.wiki }
let(:options) do
{
id: user.id,
username: user.username,
name: user.name,
email: user.email,
message: 'commit message'
}
end
subject { described_class.new(wiki, options) }
before do
project_wiki.create_page('home', 'test content')
end
shared_examples 'failing pre-receive hook' do
before do
expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([false, ''])
expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('update')
expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
end
it 'raises exception' do
expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
end
it 'does not create a new commit inside the repository' do
current_rev = find_current_rev
expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
expect(current_rev).to eq find_current_rev
end
end
shared_examples 'failing update hook' do
before do
expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([false, ''])
expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
end
it 'raises exception' do
expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
end
it 'does not create a new commit inside the repository' do
current_rev = find_current_rev
expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
expect(current_rev).to eq find_current_rev
end
end
shared_examples 'failing post-receive hook' do
before do
expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([true, ''])
expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('post-receive').and_return([false, ''])
end
it 'does not raise exception' do
expect { subject.commit }.not_to raise_error
end
it 'creates the commit' do
current_rev = find_current_rev
subject.commit
expect(current_rev).not_to eq find_current_rev
end
end
shared_examples 'when hooks call succceeds' do
let(:hook) { double(:hook) }
it 'calls the three hooks' do
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
expect(hook).to receive(:trigger).exactly(3).times.and_return([true, nil])
subject.commit
end
it 'creates the commit' do
current_rev = find_current_rev
subject.commit
expect(current_rev).not_to eq find_current_rev
end
end
context 'when creating a page' do
before do
project_wiki.create_page('index', 'test content')
end
it_behaves_like 'failing pre-receive hook'
it_behaves_like 'failing update hook'
it_behaves_like 'failing post-receive hook'
it_behaves_like 'when hooks call succceeds'
end
context 'when updating a page' do
before do
project_wiki.update_page(find_page('home'), content: 'some other content', format: :markdown)
end
it_behaves_like 'failing pre-receive hook'
it_behaves_like 'failing update hook'
it_behaves_like 'failing post-receive hook'
it_behaves_like 'when hooks call succceeds'
end
context 'when deleting a page' do
before do
project_wiki.delete_page(find_page('home'))
end
it_behaves_like 'failing pre-receive hook'
it_behaves_like 'failing update hook'
it_behaves_like 'failing post-receive hook'
it_behaves_like 'when hooks call succceeds'
end
def find_current_rev
wiki.gollum_wiki.repo.commits.first&.sha
end
def find_page(name)
wiki.page(title: name)
end
end
# TODO: Uncomment once Gitaly updates the ruby vendor code
# context 'when Gitaly is enabled' do
# it_behaves_like 'calling wiki hooks'
# end
context 'when Gitaly is disabled', :skip_gitaly_mock do
it_behaves_like 'calling wiki hooks'
end
end
......@@ -1472,7 +1472,7 @@ describe Ci::Build do
{ key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
{ key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true },
{ key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true },
{ key: 'GITLAB_FEATURES', value: project.licensed_features.join(','), public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
......
......@@ -79,9 +79,24 @@ describe GroupDescendant, :nested_groups do
expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy)
end
it 'tracks the exception when a parent was not preloaded' do
expect(Gitlab::Sentry).to receive(:track_exception).and_call_original
expect { GroupDescendant.build_hierarchy([subsub_group]) }.to raise_error(ArgumentError)
end
it 'recovers if a parent was not reloaded by querying for the parent' do
expected_hierarchy = { parent => { subgroup => subsub_group } }
# this does not raise in production, so stubbing it here.
allow(Gitlab::Sentry).to receive(:track_exception)
expect(GroupDescendant.build_hierarchy([subsub_group])).to eq(expected_hierarchy)
end
it 'raises an error if not all elements were preloaded' do
expect { described_class.build_hierarchy([subsub_group]) }
.to raise_error('parent was not preloaded')
.to raise_error(/was not preloaded/)
end
end
end
......
......@@ -377,7 +377,7 @@ describe ProjectWiki do
end
def commit_details
Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit")
end
def create_page(name, content)
......
......@@ -561,7 +561,7 @@ describe WikiPage do
end
def commit_details
Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit")
end
def create_page(name, content)
......
......@@ -217,4 +217,39 @@ describe Ci::BuildPresenter do
end
end
end
describe '#callout_failure_message' do
let(:build) { create(:ci_build, :failed, :script_failure) }
it 'returns a verbose failure reason' do
description = subject.callout_failure_message
expect(description).to eq('There has been a script failure. Check the job log for more information')
end
end
describe '#recoverable?' do
let(:build) { create(:ci_build, :failed, :script_failure) }
context 'when is a script or missing dependency failure' do
let(:failure_reasons) { %w(script_failure missing_dependency_failure) }
it 'should return false' do
failure_reasons.each do |failure_reason|
build.update_attribute(:failure_reason, failure_reason)
expect(presenter.recoverable?).to be_falsy
end
end
end
context 'when is any other failure type' do
let(:failure_reasons) { %w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure) }
it 'should return true' do
failure_reasons.each do |failure_reason|
build.update_attribute(:failure_reason, failure_reason)
expect(presenter.recoverable?).to be_truthy
end
end
end
end
end
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/has_many_through_scope'
describe RuboCop::Cop::Gitlab::HasManyThroughScope do # rubocop:disable RSpec/FilePath
include CopHelper
subject(:cop) { described_class.new }
context 'in a model file' do
before do
allow(cop).to receive(:in_model?).and_return(true)
end
context 'when the model does not use has_many :through' do
it 'does not register an offense' do
expect_no_offenses(<<-RUBY)
class User < ActiveRecord::Base
has_many :tags, source: 'UserTag'
end
RUBY
end
end
context 'when the model uses has_many :through' do
context 'when the association has no scope defined' do
it 'registers an offense on the association' do
expect_offense(<<-RUBY)
class User < ActiveRecord::Base
has_many :tags, through: :user_tags
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
end
RUBY
end
end
context 'when the association has a scope defined' do
context 'when the scope does not disable auto-loading' do
it 'registers an offense on the scope' do
expect_offense(<<-RUBY)
class User < ActiveRecord::Base
has_many :tags, -> { where(active: true) }, through: :user_tags
^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
end
RUBY
end
end
context 'when the scope has auto_include(false)' do
it 'does not register an offense' do
expect_no_offenses(<<-RUBY)
class User < ActiveRecord::Base
has_many :tags, -> { where(active: true).auto_include(false).reorder(nil) }, through: :user_tags
end
RUBY
end
end
end
end
end
context 'outside of a migration spec file' do
it 'does not register an offense' do
expect_no_offenses(<<-RUBY)
class User < ActiveRecord::Base
has_many :tags, through: :user_tags
end
RUBY
end
end
end
......@@ -133,22 +133,65 @@ describe JobEntity do
context 'when job failed' do
let(:job) { create(:ci_build, :script_failure) }
describe 'status' do
it 'should contain the failure reason inside label' do
expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
expect(subject[:status][:label]).to eq('failed')
expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)')
end
it 'contains details' do
expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
end
it 'states that it failed' do
expect(subject[:status][:label]).to eq('failed')
end
it 'should indicate the failure reason on tooltip' do
expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)')
end
it 'should include a callout message with a verbose output' do
expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information')
end
it 'should state that it is not recoverable' do
expect(subject[:recoverable]).to be_falsy
end
end
context 'when job is allowed to fail' do
let(:job) { create(:ci_build, :allowed_to_fail, :script_failure) }
it 'contains details' do
expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
end
it 'states that it failed' do
expect(subject[:status][:label]).to eq('failed (allowed to fail)')
end
it 'should indicate the failure reason on tooltip' do
expect(subject[:status][:tooltip]).to eq('failed <br> (script failure) (allowed to fail)')
end
it 'should include a callout message with a verbose output' do
expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information')
end
it 'should state that it is not recoverable' do
expect(subject[:recoverable]).to be_falsy
end
end
context 'when job failed and is recoverable' do
let(:job) { create(:ci_build, :api_failure) }
it 'should state it is recoverable' do
expect(subject[:recoverable]).to be_truthy
end
end
context 'when job passed' do
let(:job) { create(:ci_build, :success) }
describe 'status' do
it 'should not contain the failure reason inside label' do
expect(subject[:status][:label]).to eq('passed')
end
it 'should not include callout message or recoverable keys' do
expect(subject).not_to include('callout_message')
expect(subject).not_to include('recoverable')
end
end
end
......@@ -315,7 +315,9 @@ production:
mv clair-scanner_linux_amd64 clair-scanner
chmod +x clair-scanner
touch clair-whitelist.yml
while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done
retries=0
echo "Waiting for clair daemon to start"
while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
}
......
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