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 .dedicated-runner: &dedicated-runner
retry: 1 retry: 1
......
...@@ -82,7 +82,7 @@ gem 'net-ldap' ...@@ -82,7 +82,7 @@ gem 'net-ldap'
# Git Wiki # Git Wiki
# Required manually in config/initializers/gollum.rb to control load order # 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 gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false
...@@ -415,7 +415,7 @@ group :ed25519 do ...@@ -415,7 +415,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0' gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
...@@ -434,5 +434,3 @@ gem 'grape_logging', '~> 1.7' ...@@ -434,5 +434,3 @@ gem 'grape_logging', '~> 1.7'
# Asset synchronization # Asset synchronization
gem 'asset_sync', '~> 2.2.0' gem 'asset_sync', '~> 2.2.0'
gem 'goldiloader', '~> 2.0'
...@@ -290,9 +290,9 @@ GEM ...@@ -290,9 +290,9 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.94.0) gitaly-proto (0.97.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0) escape_utils (~> 1.1.0)
...@@ -331,9 +331,6 @@ GEM ...@@ -331,9 +331,6 @@ GEM
rubyntlm (~> 0.5) rubyntlm (~> 0.5)
globalid (0.4.1) globalid (0.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
goldiloader (2.0.1)
activerecord (>= 4.2, < 5.2)
activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1) gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1) gitlab-grit (~> 2.7, >= 2.7.1)
gon (6.1.0) gon (6.1.0)
...@@ -1064,7 +1061,7 @@ DEPENDENCIES ...@@ -1064,7 +1061,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.94.0) gitaly-proto (~> 0.97.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
...@@ -1072,7 +1069,6 @@ DEPENDENCIES ...@@ -1072,7 +1069,6 @@ DEPENDENCIES
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3) gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
goldiloader (~> 2.0)
gon (~> 6.1.0) gon (~> 6.1.0)
google-api-client (~> 0.19.8) google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1) google-protobuf (= 3.5.1)
......
...@@ -30,10 +30,10 @@ export default class IssuableContext { ...@@ -30,10 +30,10 @@ export default class IssuableContext {
const $selectbox = $block.find('.selectbox'); const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) { if ($selectbox.is(':visible')) {
$selectbox.hide(); $selectbox.hide();
$block.find('.value').show(); $block.find('.value:not(.dont-hide)').show();
} else { } else {
$selectbox.show(); $selectbox.show();
$block.find('.value').hide(); $block.find('.value:not(.dont-hide)').hide();
} }
if ($selectbox.is(':visible')) { if ($selectbox.is(':visible')) {
......
<script> <script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue'; import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import callout from '../../vue_shared/components/callout.vue';
export default { export default {
name: 'JobHeaderSection', name: 'JobHeaderSection',
components: { components: {
ciHeader, ciHeader,
loadingIcon, loadingIcon,
callout,
},
props: {
job: {
type: Object,
required: true,
}, },
props: { isLoading: {
job: { type: Boolean,
type: Object, required: true,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
}, },
data() { },
return { data() {
actions: this.getActions(), return {
}; actions: this.getActions(),
};
},
computed: {
status() {
return this.job && this.job.status;
}, },
computed: { shouldRenderContent() {
status() { return !this.isLoading && Object.keys(this.job).length;
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;
},
}, },
watch: { shouldRenderReason() {
job() { return !!(this.job.status && this.job.callout_message);
this.actions = this.getActions();
},
}, },
methods: { /**
getActions() { * When job has not started the key will be `false`
const actions = []; * 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) { if (this.job.new_issue_path) {
actions.push({ actions.push({
label: 'New issue', label: 'New issue',
path: this.job.new_issue_path, path: this.job.new_issue_path,
cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block', cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
type: 'link', type: 'link',
}); });
} }
return actions; return actions;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="js-build-header build-header top-area"> <header>
<ci-header <div class="js-build-header build-header top-area">
v-if="shouldRenderContent" <ci-header
:status="status" v-if="shouldRenderContent"
item-name="Job" :status="status"
:item-id="job.id" item-name="Job"
:time="job.created_at" :item-id="job.id"
:user="job.user" :time="job.created_at"
:actions="actions" :user="job.user"
:has-sidebar-button="true" :actions="actions"
:should-render-triggered-label="jobStarted" :has-sidebar-button="true"
/> :should-render-triggered-label="jobStarted"
<loading-icon />
v-if="isLoading" <loading-icon
size="2" v-if="isLoading"
class="prepend-top-default append-bottom-default" size="2"
class="prepend-top-default append-bottom-default"
/>
</div>
<callout
v-if="shouldRenderReason"
:message="job.callout_message"
/> />
</div> </header>
</template> </template>
<script> <script>
import detailRow from './sidebar_detail_row.vue'; import detailRow from './sidebar_detail_row.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago'; import timeagoMixin from '../../vue_shared/mixins/timeago';
import { timeIntervalInWords } from '../../lib/utils/datetime_utility'; import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
export default { export default {
name: 'SidebarDetailsBlock', name: 'SidebarDetailsBlock',
components: { components: {
detailRow, detailRow,
loadingIcon, loadingIcon,
},
mixins: [timeagoMixin],
props: {
job: {
type: Object,
required: true,
}, },
mixins: [ isLoading: {
timeagoMixin, type: Boolean,
], required: true,
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
}, },
computed: { canUserRetry: {
shouldRenderContent() { type: Boolean,
return !this.isLoading && Object.keys(this.job).length > 0; required: false,
}, default: false,
coverage() { },
return `${this.job.coverage}%`; runnerHelpUrl: {
}, type: String,
duration() { required: false,
return timeIntervalInWords(this.job.duration); default: '',
}, },
queued() { },
return timeIntervalInWords(this.job.queued); computed: {
}, shouldRenderContent() {
runnerId() { return !this.isLoading && Object.keys(this.job).length > 0;
return `#${this.job.runner.id}`; },
}, coverage() {
hasTimeout() { return `${this.job.coverage}%`;
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null; },
}, duration() {
timeout() { return timeIntervalInWords(this.job.duration);
if (this.job.metadata == null) { },
return ''; 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; let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') { if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`; t += ` (from ${this.job.metadata.timeout_source})`;
} }
return t; 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;
},
}, },
}; 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> </script>
<template> <template>
<div> <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"> <template v-if="shouldRenderContent">
<div <div
class="block retry-link" class="block retry-link"
...@@ -85,16 +124,16 @@ ...@@ -85,16 +124,16 @@
class="js-new-issue btn btn-new btn-inverted" class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path" :href="job.new_issue_path"
> >
New issue {{ __('New issue') }}
</a> </a>
<a <a
v-if="job.retry_path" v-if="canUserRetry"
class="js-retry-job btn btn-inverted-secondary" class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path" :href="job.retry_path"
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
> >
Retry {{ __('Retry') }}
</a> </a>
</div> </div>
<div :class="{block : renderBlock }"> <div :class="{block : renderBlock }">
...@@ -103,7 +142,7 @@ ...@@ -103,7 +142,7 @@
v-if="job.merge_request" v-if="job.merge_request"
> >
<span class="build-light-text"> <span class="build-light-text">
Merge Request: {{ __('Merge Request:') }}
</span> </span>
<a :href="job.merge_request.path"> <a :href="job.merge_request.path">
!{{ job.merge_request.iid }} !{{ job.merge_request.iid }}
...@@ -158,7 +197,7 @@ ...@@ -158,7 +197,7 @@
v-if="job.tags.length" v-if="job.tags.length"
> >
<span class="build-light-text"> <span class="build-light-text">
Tags: {{ __('Tags:') }}
</span> </span>
<span <span
v-for="(tag, i) in job.tags" v-for="(tag, i) in job.tags"
...@@ -178,7 +217,7 @@ ...@@ -178,7 +217,7 @@
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
> >
Cancel {{ __('Cancel') }}
</a> </a>
</div> </div>
</div> </div>
......
...@@ -35,9 +35,11 @@ export default () => { ...@@ -35,9 +35,11 @@ export default () => {
}); });
// Sidebar information block // Sidebar information block
const detailsBlockElement = document.getElementById('js-details-block-vue');
const detailsBlockDataset = detailsBlockElement.dataset;
// eslint-disable-next-line // eslint-disable-next-line
new Vue({ new Vue({
el: '#js-details-block-vue', el: detailsBlockElement,
components: { components: {
detailsBlock, detailsBlock,
}, },
...@@ -50,6 +52,7 @@ export default () => { ...@@ -50,6 +52,7 @@ export default () => {
return createElement('details-block', { return createElement('details-block', {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
canUserRetry: !!('canUserRetry' in detailsBlockDataset),
job: this.mediator.store.state.job, job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl, 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 @@ ...@@ -16,7 +16,7 @@
.nav-header-btn { .nav-header-btn {
padding: 10px $gl-sidebar-padding; padding: 10px $gl-sidebar-padding;
color: inherit; color: inherit;
transition-duration: .3s; transition-duration: 0.3s;
position: absolute; position: absolute;
top: 0; top: 0;
cursor: pointer; cursor: pointer;
...@@ -137,6 +137,12 @@ ...@@ -137,6 +137,12 @@
} }
} }
.issuable-sidebar .labels {
.value.dont-hide ~ .selectbox {
padding-top: $gl-padding-8;
}
}
.pikaday-container { .pikaday-container {
.pika-single { .pika-single {
margin-top: 2px; margin-top: 2px;
...@@ -151,4 +157,3 @@ ...@@ -151,4 +157,3 @@
.sidebar-collapsed-icon .sidebar-collapsed-value { .sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px; font-size: 12px;
} }
@keyframes fade-out-status { @keyframes fade-out-status {
0%, 50% { opacity: 1; } 0%,
100% { opacity: 0; } 50% {
opacity: 1;
}
100% {
opacity: 0;
}
} }
@keyframes blinking-dots { @keyframes blinking-dots {
0% { 0% {
background-color: rgba($white-light, 1); background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 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% { 25% {
background-color: rgba($white-light, 0.4); background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 2), 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% { 75% {
background-color: rgba($white-light, 0.4); background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 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% { 100% {
background-color: rgba($white-light, 1); background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 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 { @keyframes blinking-scroll-button {
0% { opacity: 0.2; } 0% {
25% { opacity: 0.5; } opacity: 0.2;
50% { opacity: 0.7; } }
100% { opacity: 1; }
25% {
opacity: 0.5;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
} }
.build-page { .build-page {
...@@ -125,12 +142,12 @@ ...@@ -125,12 +142,12 @@
.btn-scroll.animate { .btn-scroll.animate {
.first-triangle { .first-triangle {
animation: blinking-scroll-button 1s ease infinite; animation: blinking-scroll-button 1s ease infinite;
animation-delay: .3s; animation-delay: 0.3s;
} }
.second-triangle { .second-triangle {
animation: blinking-scroll-button 1s ease infinite; animation: blinking-scroll-button 1s ease infinite;
animation-delay: .2s; animation-delay: 0.2s;
} }
.third-triangle { .third-triangle {
......
...@@ -78,6 +78,8 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -78,6 +78,8 @@ class Projects::JobsController < Projects::ApplicationController
result.merge!(trace.to_h) result.merge!(trace.to_h)
end end
result[:html] = result[:html].presence || 'No job log'
render json: result render json: result
end end
end end
......
...@@ -29,8 +29,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -29,8 +29,7 @@ class Projects::WikisController < Projects::ApplicationController
else else
return render('empty') unless can?(current_user, :create_wiki, @project) return render('empty') unless can?(current_user, :create_wiki, @project)
@page = WikiPage.new(@project_wiki) @page = build_page(title: params[:id])
@page.title = params[:id]
render 'edit' render 'edit'
end end
...@@ -54,7 +53,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -54,7 +53,7 @@ class Projects::WikisController < Projects::ApplicationController
else else
render 'edit' render 'edit'
end end
rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e
@error = e @error = e
render 'edit' render 'edit'
end end
...@@ -70,6 +69,11 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -70,6 +69,11 @@ class Projects::WikisController < Projects::ApplicationController
else else
render action: "edit" render action: "edit"
end end
rescue Gitlab::Git::Wiki::OperationError => e
@page = build_page(wiki_params)
@error = e
render 'edit'
end end
def history def history
...@@ -94,6 +98,9 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -94,6 +98,9 @@ class Projects::WikisController < Projects::ApplicationController
redirect_to project_wiki_path(@project, :home), redirect_to project_wiki_path(@project, :home),
status: 302, status: 302,
notice: "Page was successfully deleted" notice: "Page was successfully deleted"
rescue Gitlab::Git::Wiki::OperationError => e
@error = e
render 'edit'
end end
def git_access def git_access
...@@ -116,4 +123,10 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -116,4 +123,10 @@ class Projects::WikisController < Projects::ApplicationController
def wiki_params def wiki_params
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha) params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end end
def build_page(args)
WikiPage.new(@project_wiki).tap do |page|
page.update_attributes(args)
end
end
end end
...@@ -134,10 +134,8 @@ class GroupDescendantsFinder ...@@ -134,10 +134,8 @@ class GroupDescendantsFinder
end end
def direct_child_projects def direct_child_projects
GroupProjectsFinder.new(group: parent_group, GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params)
current_user: current_user, .execute
options: { only_owned: true },
params: params).execute
end end
# Finds all projects nested under `parent_group` or any of its descendant # Finds all projects nested under `parent_group` or any of its descendant
......
...@@ -28,7 +28,7 @@ module NavHelper ...@@ -28,7 +28,7 @@ module NavHelper
end end
elsif current_path?('jobs#show') elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded] %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] %w[page-gutter wiki-sidebar right-sidebar-expanded]
else else
[] []
......
...@@ -611,7 +611,7 @@ module Ci ...@@ -611,7 +611,7 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables| Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true') variables.append(key: 'CI', value: 'true')
variables.append(key: 'GITLAB_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_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION) variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION) variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
......
...@@ -13,7 +13,7 @@ module Ci ...@@ -13,7 +13,7 @@ module Ci
has_many :builds has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent 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' has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
......
...@@ -15,7 +15,7 @@ module Clusters ...@@ -15,7 +15,7 @@ module Clusters
belongs_to :user belongs_to :user
has_many :cluster_projects, class_name: 'Clusters::Project' 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 # we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
......
...@@ -37,7 +37,20 @@ module GroupDescendant ...@@ -37,7 +37,20 @@ module GroupDescendant
parent ||= preloaded.detect { |possible_parent| possible_parent.is_a?(Group) && possible_parent.id == child.parent_id } parent ||= preloaded.detect { |possible_parent| possible_parent.is_a?(Group) && possible_parent.id == child.parent_id }
if parent.nil? && !child.parent_id.nil? 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 end
if parent.nil? && hierarchy_top.present? if parent.nil? && hierarchy_top.present?
......
...@@ -48,7 +48,7 @@ module Issuable ...@@ -48,7 +48,7 @@ module Issuable
end end
has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent 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_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :metrics has_one :metrics
......
...@@ -102,7 +102,7 @@ module ResolvableDiscussion ...@@ -102,7 +102,7 @@ module ResolvableDiscussion
yield(notes_relation) yield(notes_relation)
# Set the notes array to the updated notes # 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| self.class.memoized_values.each do |name|
clear_memoization(name) clear_memoization(name)
......
...@@ -2,7 +2,7 @@ class DeployKey < Key ...@@ -2,7 +2,7 @@ class DeployKey < Key
include IgnorableColumn include IgnorableColumn
has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent 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 :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) }
scope :are_public, -> { where(public: true) } scope :are_public, -> { where(public: true) }
......
...@@ -8,7 +8,7 @@ class DeployToken < ActiveRecord::Base ...@@ -8,7 +8,7 @@ class DeployToken < ActiveRecord::Base
default_value_for(:expires_at) { Forever.date } default_value_for(:expires_at) { Forever.date }
has_many :project_deploy_tokens, inverse_of: :deploy_token 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 validate :ensure_at_least_one_scope
before_save :ensure_token before_save :ensure_token
......
class ForkNetwork < ActiveRecord::Base class ForkNetwork < ActiveRecord::Base
belongs_to :root_project, class_name: 'Project' belongs_to :root_project, class_name: 'Project'
has_many :fork_network_members 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 after_create :add_root_as_member, if: :root_project
......
...@@ -12,9 +12,9 @@ class Group < Namespace ...@@ -12,9 +12,9 @@ class Group < Namespace
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
has_many :users, -> { auto_include(false) }, through: :group_members has_many :users, through: :group_members
has_many :owners, has_many :owners,
-> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) }, -> { where(members: { access_level: Gitlab::Access::OWNER }) },
through: :group_members, through: :group_members,
source: :user source: :user
...@@ -23,7 +23,7 @@ class Group < Namespace ...@@ -23,7 +23,7 @@ class Group < Namespace
has_many :milestones has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent 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 :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel' has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable' has_many :variables, class_name: 'Ci::GroupVariable'
......
...@@ -34,7 +34,7 @@ class Issue < ActiveRecord::Base ...@@ -34,7 +34,7 @@ class Issue < ActiveRecord::Base
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees 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 validates :project, presence: true
......
...@@ -18,8 +18,8 @@ class Label < ActiveRecord::Base ...@@ -18,8 +18,8 @@ class Label < ActiveRecord::Base
has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :priorities, class_name: 'LabelPriority' has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent 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 :issues, 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 :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
before_validation :strip_whitespace_from_title_and_color before_validation :strip_whitespace_from_title_and_color
......
...@@ -3,7 +3,7 @@ class LfsObject < ActiveRecord::Base ...@@ -3,7 +3,7 @@ class LfsObject < ActiveRecord::Base
include ObjectStorage::BackgroundMove include ObjectStorage::BackgroundMove
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent 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]) } scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
......
...@@ -22,7 +22,7 @@ class Milestone < ActiveRecord::Base ...@@ -22,7 +22,7 @@ class Milestone < ActiveRecord::Base
belongs_to :group belongs_to :group
has_many :issues 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 :merge_requests
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......
...@@ -248,10 +248,6 @@ class Namespace < ActiveRecord::Base ...@@ -248,10 +248,6 @@ class Namespace < ActiveRecord::Base
all_projects.with_storage_feature(:repository).find_each(&:remove_exports) all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
end end
def features
[]
end
def refresh_project_authorizations def refresh_project_authorizations
owner.refresh_authorized_projects owner.refresh_authorized_projects
end end
......
...@@ -138,11 +138,11 @@ class Project < ActiveRecord::Base ...@@ -138,11 +138,11 @@ class Project < ActiveRecord::Base
has_one :packagist_service has_one :packagist_service
# TODO: replace these relations with the fork network versions # TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id" 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_from_project, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id" 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 # TODO: replace these relations with the fork network versions
has_one :root_of_fork_network, has_one :root_of_fork_network,
...@@ -150,7 +150,7 @@ class Project < ActiveRecord::Base ...@@ -150,7 +150,7 @@ class Project < ActiveRecord::Base
inverse_of: :root_project, inverse_of: :root_project,
class_name: 'ForkNetwork' class_name: 'ForkNetwork'
has_one :fork_network_member 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 # Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id' has_many :merge_requests, foreign_key: 'target_project_id'
...@@ -167,27 +167,27 @@ class Project < ActiveRecord::Base ...@@ -167,27 +167,27 @@ class Project < ActiveRecord::Base
has_many :protected_tags has_many :protected_tags
has_many :project_authorizations 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) }, has_many :project_members, -> { where(requested_at: nil) },
as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :project_members 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) }, has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :members_and_requesters, as: :source, class_name: 'ProjectMember' has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
has_many :deploy_keys_projects 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 :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 :releases
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent 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 :lfs_file_locks
has_many :project_group_links 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 :pages_domains
has_many :todos has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
...@@ -199,7 +199,7 @@ class Project < ActiveRecord::Base ...@@ -199,7 +199,7 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics' has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project' 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, # Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy # which is not managed by the DB. Hence we're still using dependent: :destroy
...@@ -216,16 +216,16 @@ class Project < ActiveRecord::Base ...@@ -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 :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 :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :runner_projects, class_name: 'Ci::RunnerProject' 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 :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger' has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments has_many :environments
has_many :deployments has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule' has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens 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_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute' has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
...@@ -1875,6 +1875,10 @@ class Project < ActiveRecord::Base ...@@ -1875,6 +1875,10 @@ class Project < ActiveRecord::Base
memoized_results[cache_key] memoized_results[cache_key]
end end
def licensed_features
[]
end
private private
def storage def storage
......
...@@ -179,7 +179,11 @@ class ProjectWiki ...@@ -179,7 +179,11 @@ class ProjectWiki
def commit_details(action, message = nil, title = nil) def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title) 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 end
def default_message(action, title) def default_message(action, title)
......
...@@ -22,7 +22,7 @@ class Todo < ActiveRecord::Base ...@@ -22,7 +22,7 @@ class Todo < ActiveRecord::Base
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :note belongs_to :note
belongs_to :project 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 belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :name, :email, to: :author, prefix: true, allow_nil: true
......
...@@ -96,23 +96,23 @@ class User < ActiveRecord::Base ...@@ -96,23 +96,23 @@ class User < ActiveRecord::Base
# Groups # Groups
has_many :members has_many :members
has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember' has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
has_many :groups, -> { auto_include(false) }, through: :group_members has_many :groups, through: :group_members
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) }, through: :group_members, source: :group 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 }).auto_include(false) }, through: :group_members, source: :group has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }) }, through: :group_members, source: :group
# Projects # Projects
has_many :groups_projects, -> { auto_include(false) }, through: :groups, source: :projects has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, -> { auto_include(false) }, through: :namespace, source: :projects has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, -> { where(requested_at: nil) } has_many :project_members, -> { where(requested_at: nil) }
has_many :projects, -> { auto_include(false) }, through: :project_members has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent 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 :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 :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 :snippets, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :notes, 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 ...@@ -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 :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees 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 :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute' has_many :custom_attributes, class_name: 'UserCustomAttribute'
......
...@@ -265,6 +265,15 @@ class WikiPage ...@@ -265,6 +265,15 @@ class WikiPage
title.present? && self.class.unhyphenize(@page.url_path) != title title.present? && self.class.unhyphenize(@page.url_path) != title
end 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 private
# Process and format the title based on the user input. # Process and format the title based on the user input.
...@@ -290,15 +299,6 @@ class WikiPage ...@@ -290,15 +299,6 @@ class WikiPage
File.join(components) File.join(components)
end 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 def set_attributes
attributes[:slug] = @page.url_path attributes[:slug] = @page.url_path
attributes[:title] = @page.title attributes[:title] = @page.title
......
module Ci module Ci
class BuildPresenter < Gitlab::View::Presenter::Delegated 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 presents :build
def erased_by_user? def erased_by_user?
...@@ -35,6 +44,14 @@ module Ci ...@@ -35,6 +44,14 @@ 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
...@@ -44,5 +61,9 @@ module Ci ...@@ -44,5 +61,9 @@ 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
...@@ -26,6 +26,8 @@ class JobEntity < Grape::Entity ...@@ -26,6 +26,8 @@ class JobEntity < Grape::Entity
expose :created_at expose :created_at
expose :updated_at expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity expose :detailed_status, as: :status, with: StatusEntity
expose :callout_message, if: -> (*) { failed? }
expose :recoverable, if: -> (*) { failed? }
private private
...@@ -50,4 +52,20 @@ class JobEntity < Grape::Entity ...@@ -50,4 +52,20 @@ class JobEntity < Grape::Entity
def path_to(route, build) def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend
end 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 end
...@@ -33,7 +33,7 @@ module Ci ...@@ -33,7 +33,7 @@ module Ci
end end
end end
builds.auto_include(false).find do |build| builds.find do |build|
next unless runner.can_pick?(build) next unless runner.can_pick?(build)
begin begin
......
<%= yield -%> <%= yield -%>
--- -- <%# signature marker %>
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
<%= yield -%> <%= yield -%>
--- -- <%# signature marker %>
<% if @target_url -%> <% if @target_url -%>
<% if @reply_by_email -%> <% if @reply_by_email -%>
<%= "Reply to this email directly or view it on GitLab: #{@target_url}" -%> <%= "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" } } %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container .sidebar-container
.blocks-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?) - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block .block
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- if can_admin_issue? - if can_admin_issue?
= icon("spinner spin", class: "block-loading") = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" = 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" } %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
None None
%a{ href: "#", %a{ href: "#",
......
...@@ -96,7 +96,7 @@ ...@@ -96,7 +96,7 @@
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable - if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' = 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? - if selected_labels.any?
- selected_labels.each do |label| - selected_labels.each do |label|
= link_to_label(label, subject: issuable.project, type: issuable.to_ability_name) = 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: ...@@ -17,7 +17,11 @@ codequality:
- docker:stable-dind - docker:stable-dind
script: script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - 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: artifacts:
paths: [codeclimate.json] paths: [codeclimate.json]
``` ```
......
...@@ -42,9 +42,9 @@ dast: ...@@ -42,9 +42,9 @@ dast:
allow_failure: true allow_failure: true
script: script:
- mkdir /zap/wrk/ - mkdir /zap/wrk/
- /zap/zap-baseline.py -J gl-dast-report.json -t $website \ - /zap/zap-baseline.py -J gl-dast-report.json -t $website
--auth-url $login_url \ --auth-url $login_url
--auth-username "john.doe@example.com" \ --auth-username "john.doe@example.com"
--auth-password "john-doe-password" || true --auth-password "john-doe-password" || true
- cp /zap/wrk/gl-dast-report.json . - cp /zap/wrk/gl-dast-report.json .
artifacts: artifacts:
......
...@@ -75,7 +75,7 @@ cancel the job, retry it, or erase the job trace. ...@@ -75,7 +75,7 @@ cancel the job, retry it, or erase the job trace.
## Seeing the failure reason for jobs ## 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 When a pipeline fails or is allowed to fail, there are several places where you
can quickly check the reason it failed: 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. ...@@ -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) ![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 ## Pipeline graphs
> [Introduced][ce-5742] in GitLab 8.11. > [Introduced][ce-5742] in GitLab 8.11.
...@@ -279,4 +281,5 @@ runners will not use regular runners, they must be tagged accordingly. ...@@ -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-7931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931
[ce-9760]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760 [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-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 [regexp]: https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99
...@@ -133,11 +133,19 @@ roughly be as follows: ...@@ -133,11 +133,19 @@ roughly be as follows:
1. Release B: 1. Release B:
1. Deploy code so that the application starts using the new column and stops 1. Deploy code so that the application starts using the new column and stops
scheduling jobs for newly created data. scheduling jobs for newly created data.
1. In a post-deployment migration you'll need to ensure no jobs remain. To do 1. In a post-deployment migration you'll need to ensure no jobs remain.
so you can use `Gitlab::BackgroundMigration.steal` to process any remaining 1. Use `Gitlab::BackgroundMigration.steal` to process any remaining
jobs before continuing. 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. 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 ## Example
To explain all this, let's use the following example: the table `services` has a To explain all this, let's use the following example: the table `services` has a
...@@ -296,3 +304,4 @@ for more details. ...@@ -296,3 +304,4 @@ for more details.
[migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md [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 [issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351
[reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-ce/issues/36791 [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 ...@@ -2,10 +2,11 @@ module Gitlab
module Git module Git
class Wiki class Wiki
DuplicatePageError = Class.new(StandardError) 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 def to_h
{ name: name, email: email, message: message } { user_id: user_id, username: username, name: name, email: email, message: message }
end end
end end
PageBlob = Struct.new(:name) PageBlob = Struct.new(:name)
...@@ -140,6 +141,10 @@ module Gitlab ...@@ -140,6 +141,10 @@ module Gitlab
end end
end end
def gollum_wiki
@gollum_wiki ||= Gollum::Wiki.new(@repository.path)
end
private private
# options: # options:
...@@ -158,10 +163,6 @@ module Gitlab ...@@ -158,10 +163,6 @@ module Gitlab
offset: options[:offset]) offset: options[:offset])
end end
def gollum_wiki
@gollum_wiki ||= Gollum::Wiki.new(@repository.path)
end
def gollum_page_by_path(page_path) def gollum_page_by_path(page_path)
page_name = Gollum::Page.canonicalize_filename(page_path) page_name = Gollum::Page.canonicalize_filename(page_path)
page_dir = File.split(page_path).first page_dir = File.split(page_path).first
...@@ -201,12 +202,12 @@ module Gitlab ...@@ -201,12 +202,12 @@ module Gitlab
assert_type!(format, Symbol) assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails) assert_type!(commit_details, CommitDetails)
filename = File.basename(name) with_committer_with_hooks(commit_details) do |committer|
dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir filename = File.basename(name)
dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
gollum_wiki.write_page(filename, format, content, commit_details.to_h, dir)
nil gollum_wiki.write_page(filename, format, content, { committer: committer }, dir)
end
rescue Gollum::DuplicatePageError => e rescue Gollum::DuplicatePageError => e
raise Gitlab::Git::Wiki::DuplicatePageError, e.message raise Gitlab::Git::Wiki::DuplicatePageError, e.message
end end
...@@ -214,24 +215,23 @@ module Gitlab ...@@ -214,24 +215,23 @@ module Gitlab
def gollum_delete_page(page_path, commit_details) def gollum_delete_page(page_path, commit_details)
assert_type!(commit_details, CommitDetails) assert_type!(commit_details, CommitDetails)
gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h) with_committer_with_hooks(commit_details) do |committer|
nil gollum_wiki.delete_page(gollum_page_by_path(page_path), committer: committer)
end
end end
def gollum_update_page(page_path, title, format, content, commit_details) def gollum_update_page(page_path, title, format, content, commit_details)
assert_type!(format, Symbol) assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails) assert_type!(commit_details, CommitDetails)
page = gollum_page_by_path(page_path) with_committer_with_hooks(commit_details) do |committer|
committer = Gollum::Committer.new(page.wiki, commit_details.to_h) page = gollum_page_by_path(page_path)
# Instead of performing two renames if the title has changed,
# Instead of performing two renames if the title has changed, # the update_page will only update the format and content and
# the update_page will only update the format and content and # the rename_page will do anything related to moving/renaming
# the rename_page will do anything related to moving/renaming gollum_wiki.update_page(page, page.name, format, content, committer: committer)
gollum_wiki.update_page(page, page.name, format, content, committer: committer) gollum_wiki.rename_page(page, title, committer: committer)
gollum_wiki.rename_page(page, title, committer: committer) end
committer.commit
nil
end end
def gollum_find_page(title:, version: nil, dir: nil) def gollum_find_page(title:, version: nil, dir: nil)
...@@ -288,6 +288,20 @@ module Gitlab ...@@ -288,6 +288,20 @@ module Gitlab
Gitlab::Git::WikiPage.new(wiki_page, version) Gitlab::Git::WikiPage.new(wiki_page, version)
end end
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 end
end end
...@@ -200,6 +200,8 @@ module Gitlab ...@@ -200,6 +200,8 @@ module Gitlab
def gitaly_commit_details(commit_details) def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new( Gitaly::WikiCommitDetails.new(
user_id: commit_details.user_id,
user_name: encode_binary(commit_details.username),
name: encode_binary(commit_details.name), name: encode_binary(commit_details.name),
email: encode_binary(commit_details.email), email: encode_binary(commit_details.email),
message: encode_binary(commit_details.message) message: encode_binary(commit_details.message)
......
...@@ -2,10 +2,14 @@ module Gitlab ...@@ -2,10 +2,14 @@ module Gitlab
module GlId module GlId
def self.gl_id(user) def self.gl_id(user)
if user.present? if user.present?
"user-#{user.id}" gl_id_from_id_value(user.id)
else else
"" ''
end end
end end
def self.gl_id_from_id_value(id)
"user-#{id}"
end
end end
end end
...@@ -20,6 +20,10 @@ module Gitlab ...@@ -20,6 +20,10 @@ module Gitlab
subject subject
end end
def present(**attributes)
self
end
class_methods do class_methods do
def presenter? def presenter?
true 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 # 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/module_with_instance_variables'
require_relative 'cop/gitlab/predicate_memoization' require_relative 'cop/gitlab/predicate_memoization'
require_relative 'cop/gitlab/httparty'
require_relative 'cop/include_sidekiq_worker' require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/avoid_return_from_blocks' require_relative 'cop/avoid_return_from_blocks'
require_relative 'cop/avoid_break_from_strong_memoize' require_relative 'cop/avoid_break_from_strong_memoize'
......
...@@ -190,7 +190,10 @@ describe Projects::JobsController do ...@@ -190,7 +190,10 @@ describe Projects::JobsController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status 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
end end
......
...@@ -243,5 +243,10 @@ FactoryBot.define do ...@@ -243,5 +243,10 @@ FactoryBot.define do
failed failed
failure_reason 1 failure_reason 1
end end
trait :api_failure do
failed
failure_reason 2
end
end end
end end
...@@ -237,6 +237,22 @@ describe 'Issue Boards', :js do ...@@ -237,6 +237,22 @@ describe 'Issue Boards', :js do
end end
context 'labels' do 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 it 'adds a single label' do
click_card(card) click_card(card)
...@@ -296,7 +312,9 @@ describe 'Issue Boards', :js do ...@@ -296,7 +312,9 @@ describe 'Issue Boards', :js do
wait_for_requests wait_for_requests
click_link stretch.title within('.dropdown-menu-labels') do
click_link stretch.title
end
wait_for_requests wait_for_requests
......
...@@ -5,9 +5,9 @@ feature 'Issue Sidebar' do ...@@ -5,9 +5,9 @@ feature 'Issue Sidebar' do
let(:group) { create(:group, :nested) } let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) } let(:project) { create(:project, :public, namespace: group) }
let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)} let!(:user) { create(:user)}
let!(:label) { create(:label, project: project, title: 'bug') } 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;') } let!(:xss_label) { create(:label, project: project, title: '&lt;script&gt;alert("xss");&lt;&#x2F;script&gt;') }
before do before do
...@@ -112,11 +112,18 @@ feature 'Issue Sidebar' do ...@@ -112,11 +112,18 @@ feature 'Issue Sidebar' do
context 'editing issue labels', :js do context 'editing issue labels', :js do
before do before do
issue.update_attributes(labels: [label])
page.within('.block.labels') do page.within('.block.labels') do
find('.edit-link').click find('.edit-link').click
end end
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 it 'shows option to create a project label' do
page.within('.block.labels') do page.within('.block.labels') do
expect(page).to have_content 'Create project' expect(page).to have_content 'Create project'
......
...@@ -491,16 +491,18 @@ feature 'Jobs' do ...@@ -491,16 +491,18 @@ feature 'Jobs' do
end end
end end
describe "POST /:project/jobs/:id/retry" do describe "POST /:project/jobs/:id/retry", :js do
context "Job from project", :js do context "Job from project", :js do
before do before do
job.run! job.run!
job.cancel!
visit project_job_path(project, job) visit project_job_path(project, job)
find('.js-cancel-job').click() wait_for_requests
find('.js-retry-button').click find('.js-retry-button').click
end end
it 'shows the right status and buttons', :js do it 'shows the right status and buttons' do
page.within('aside.right-sidebar') do page.within('aside.right-sidebar') do
expect(page).to have_content 'Cancel' expect(page).to have_content 'Cancel'
end end
......
...@@ -35,15 +35,6 @@ describe GroupDescendantsFinder do ...@@ -35,15 +35,6 @@ describe GroupDescendantsFinder do
expect(finder.execute).to contain_exactly(project) expect(finder.execute).to contain_exactly(project)
end 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 context 'when archived is `true`' do
let(:params) { { archived: 'true' } } let(:params) { { archived: 'true' } }
......
...@@ -36,14 +36,28 @@ describe('Job details header', () => { ...@@ -36,14 +36,28 @@ describe('Job details header', () => {
}, },
isLoading: false, isLoading: false,
}; };
vm = mountComponent(HeaderComponent, props);
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); 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', () => { describe('triggered job', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(HeaderComponent, props); vm = mountComponent(HeaderComponent, props);
...@@ -51,14 +65,17 @@ describe('Job details header', () => { ...@@ -51,14 +65,17 @@ describe('Job details header', () => {
it('should render provided job information', () => { it('should render provided job information', () => {
expect( 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'); ).toEqual('failed Job #123 triggered 3 weeks ago by Foo');
}); });
it('should render new issue link', () => { it('should render new issue link', () => {
expect( expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
vm.$el.querySelector('.js-new-issue').getAttribute('href'), props.job.new_issue_path,
).toEqual(props.job.new_issue_path); );
}); });
}); });
...@@ -68,7 +85,10 @@ describe('Job details header', () => { ...@@ -68,7 +85,10 @@ describe('Job details header', () => {
vm = mountComponent(HeaderComponent, props); vm = mountComponent(HeaderComponent, props);
expect( 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'); ).toEqual('failed Job #123 created 3 weeks ago by Foo');
}); });
}); });
......
...@@ -31,10 +31,25 @@ describe('Sidebar details block', () => { ...@@ -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(() => { beforeEach(() => {
vm = new SidebarComponent({ vm = new SidebarComponent({
propsData: { propsData: {
job, job,
canUserRetry: true,
isLoading: false, isLoading: false,
}, },
}).$mount(); }).$mount();
...@@ -42,7 +57,9 @@ describe('Sidebar details block', () => { ...@@ -42,7 +57,9 @@ describe('Sidebar details block', () => {
describe('actions', () => { describe('actions', () => {
it('should render link to new issue', () => { 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'); expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue');
}); });
...@@ -57,43 +74,35 @@ describe('Sidebar details block', () => { ...@@ -57,43 +74,35 @@ describe('Sidebar details block', () => {
describe('information', () => { describe('information', () => {
it('should render merge request link', () => { it('should render merge request link', () => {
expect( expect(trimWhitespace(vm.$el.querySelector('.js-job-mr'))).toEqual('Merge Request: !2');
trimWhitespace(vm.$el.querySelector('.js-job-mr')),
).toEqual('Merge Request: !2');
expect( expect(vm.$el.querySelector('.js-job-mr a').getAttribute('href')).toEqual(
vm.$el.querySelector('.js-job-mr a').getAttribute('href'), job.merge_request.path,
).toEqual(job.merge_request.path); );
}); });
it('should render job duration', () => { it('should render job duration', () => {
expect( expect(trimWhitespace(vm.$el.querySelector('.js-job-duration'))).toEqual(
trimWhitespace(vm.$el.querySelector('.js-job-duration')), 'Duration: 6 seconds',
).toEqual('Duration: 6 seconds'); );
}); });
it('should render erased date', () => { it('should render erased date', () => {
expect( expect(trimWhitespace(vm.$el.querySelector('.js-job-erased'))).toEqual('Erased: 3 weeks ago');
trimWhitespace(vm.$el.querySelector('.js-job-erased')),
).toEqual('Erased: 3 weeks ago');
}); });
it('should render finished date', () => { it('should render finished date', () => {
expect( expect(trimWhitespace(vm.$el.querySelector('.js-job-finished'))).toEqual(
trimWhitespace(vm.$el.querySelector('.js-job-finished')), 'Finished: 3 weeks ago',
).toEqual('Finished: 3 weeks ago'); );
}); });
it('should render queued date', () => { it('should render queued date', () => {
expect( expect(trimWhitespace(vm.$el.querySelector('.js-job-queued'))).toEqual('Queued: 9 seconds');
trimWhitespace(vm.$el.querySelector('.js-job-queued')),
).toEqual('Queued: 9 seconds');
}); });
it('should render runner ID', () => { it('should render runner ID', () => {
expect( expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: #1');
trimWhitespace(vm.$el.querySelector('.js-job-runner')),
).toEqual('Runner: #1');
}); });
it('should render timeout information', () => { it('should render timeout information', () => {
...@@ -103,15 +112,11 @@ describe('Sidebar details block', () => { ...@@ -103,15 +112,11 @@ describe('Sidebar details block', () => {
}); });
it('should render coverage', () => { it('should render coverage', () => {
expect( expect(trimWhitespace(vm.$el.querySelector('.js-job-coverage'))).toEqual('Coverage: 20%');
trimWhitespace(vm.$el.querySelector('.js-job-coverage')),
).toEqual('Coverage: 20%');
}); });
it('should render tags', () => { it('should render tags', () => {
expect( expect(trimWhitespace(vm.$el.querySelector('.js-job-tags'))).toEqual('Tags: tag');
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 ...@@ -30,7 +30,7 @@ describe Gitlab::Git::Wiki do
end end
def commit_details(name) 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 end
def destroy_page(title, dir = '') def destroy_page(title, dir = '')
......
...@@ -48,4 +48,11 @@ describe Gitlab::View::Presenter::Base do ...@@ -48,4 +48,11 @@ describe Gitlab::View::Presenter::Base do
end end
end 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 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 ...@@ -1472,7 +1472,7 @@ describe Ci::Build do
{ key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false }, { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
{ key: 'CI', value: 'true', public: true }, { key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_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_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
......
...@@ -79,9 +79,24 @@ describe GroupDescendant, :nested_groups do ...@@ -79,9 +79,24 @@ describe GroupDescendant, :nested_groups do
expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy) expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy)
end 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 it 'raises an error if not all elements were preloaded' do
expect { described_class.build_hierarchy([subsub_group]) } expect { described_class.build_hierarchy([subsub_group]) }
.to raise_error('parent was not preloaded') .to raise_error(/was not preloaded/)
end end
end end
end end
......
...@@ -377,7 +377,7 @@ describe ProjectWiki do ...@@ -377,7 +377,7 @@ describe ProjectWiki do
end end
def commit_details 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 end
def create_page(name, content) def create_page(name, content)
......
...@@ -561,7 +561,7 @@ describe WikiPage do ...@@ -561,7 +561,7 @@ describe WikiPage do
end end
def commit_details 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 end
def create_page(name, content) def create_page(name, content)
......
...@@ -217,4 +217,39 @@ describe Ci::BuildPresenter do ...@@ -217,4 +217,39 @@ describe Ci::BuildPresenter do
end end
end 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 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 ...@@ -133,22 +133,65 @@ describe JobEntity do
context 'when job failed' do context 'when job failed' do
let(:job) { create(:ci_build, :script_failure) } let(:job) { create(:ci_build, :script_failure) }
describe 'status' do it 'contains details' do
it 'should contain the failure reason inside label' do expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip end
expect(subject[:status][:label]).to eq('failed')
expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)') it 'states that it failed' do
end 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
end end
context 'when job passed' do context 'when job passed' do
let(:job) { create(:ci_build, :success) } let(:job) { create(:ci_build, :success) }
describe 'status' do it 'should not include callout message or recoverable keys' do
it 'should not contain the failure reason inside label' do expect(subject).not_to include('callout_message')
expect(subject[:status][:label]).to eq('passed') expect(subject).not_to include('recoverable')
end
end end
end end
end end
...@@ -315,7 +315,9 @@ production: ...@@ -315,7 +315,9 @@ production:
mv clair-scanner_linux_amd64 clair-scanner mv clair-scanner_linux_amd64 clair-scanner
chmod +x clair-scanner chmod +x clair-scanner
touch clair-whitelist.yml 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 ./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