Commit e809bbc8 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-04-06

# Conflicts:
#	app/assets/javascripts/pipelines/components/graph/graph_component.vue
#	app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
#	app/assets/stylesheets/pages/environments.scss
#	config/prometheus/additional_metrics.yml
#	lib/gitlab/git/repository.rb
#	spec/lib/gitlab/git_access_spec.rb
#	spec/migrations/remove_soft_removed_objects_spec.rb
#	spec/spec_helper.rb

[ci skip]
parents f89c3101 85f4e323
...@@ -59,6 +59,8 @@ linters: ...@@ -59,6 +59,8 @@ linters:
# Reports when you define the same property twice in a single rule set. # Reports when you define the same property twice in a single rule set.
DuplicateProperty: DuplicateProperty:
enabled: true enabled: true
ignore_consecutive:
- cursor
# Separate rule, function, and mixin declarations with empty lines. # Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks: EmptyLineBetweenBlocks:
......
...@@ -36,6 +36,7 @@ export default { ...@@ -36,6 +36,7 @@ export default {
> >
<a <a
v-tooltip v-tooltip
v-if="!file.binary"
:href="file.blamePath" :href="file.blamePath"
:title="__('Blame')" :title="__('Blame')"
class="btn btn-xs btn-transparent blame" class="btn btn-xs btn-transparent blame"
......
<script> <script>
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
export default { export default {
components: { components: {
icon, icon,
},
directives: {
tooltip,
},
mixins: [timeAgoMixin],
props: {
file: {
type: Object,
required: true,
}, },
directives: { },
tooltip, };
},
mixins: [
timeAgoMixin,
],
props: {
file: {
type: Object,
required: true,
},
},
};
</script> </script>
<template> <template>
...@@ -50,7 +48,9 @@ ...@@ -50,7 +48,9 @@
<div class="text-right"> <div class="text-right">
{{ file.eol }} {{ file.eol }}
</div> </div>
<div class="text-right"> <div
class="text-right"
v-if="!file.binary">
{{ file.editorRow }}:{{ file.editorColumn }} {{ file.editorRow }}:{{ file.editorColumn }}
</div> </div>
<div class="text-right"> <div class="text-right">
......
...@@ -171,10 +171,10 @@ export default { ...@@ -171,10 +171,10 @@ export default {
id="ide" id="ide"
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div <div class="ide-mode-tabs clearfix">
class="ide-mode-tabs clearfix" <ul
v-if="!shouldHideEditor"> class="nav-links pull-left"
<ul class="nav-links pull-left"> v-if="!shouldHideEditor">
<li :class="editTabCSS"> <li :class="editTabCSS">
<a <a
href="javascript:void(0);" href="javascript:void(0);"
...@@ -210,9 +210,10 @@ export default { ...@@ -210,9 +210,10 @@ export default {
> >
</div> </div>
<content-viewer <content-viewer
v-if="!shouldHideEditor && file.viewMode === 'preview'" v-if="shouldHideEditor || file.viewMode === 'preview'"
:content="file.content || file.raw" :content="file.content || file.raw"
:path="file.path" :path="file.rawPath"
:file-size="file.size"
:project-path="file.projectId"/> :project-path="file.projectId"/>
</div> </div>
</template> </template>
...@@ -43,6 +43,7 @@ export default { ...@@ -43,6 +43,7 @@ export default {
raw: null, raw: null,
baseRaw: null, baseRaw: null,
html: data.html, html: data.html,
size: data.size,
}); });
}, },
[types.SET_FILE_RAW_DATA](state, { file, raw }) { [types.SET_FILE_RAW_DATA](state, { file, raw }) {
......
...@@ -40,6 +40,7 @@ export const dataStructure = () => ({ ...@@ -40,6 +40,7 @@ export const dataStructure = () => ({
eol: '', eol: '',
viewMode: 'edit', viewMode: 'edit',
previewMode: null, previewMode: null,
size: 0,
}); });
export const decorateData = entity => { export const decorateData = entity => {
......
<script> <script>
import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import StageColumnComponent from './stage_column_component.vue'; import StageColumnComponent from './stage_column_component.vue';
<<<<<<< HEAD
import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue'; // eslint-disable-line import/first import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue'; // eslint-disable-line import/first
export default { export default {
...@@ -9,6 +10,15 @@ export default { ...@@ -9,6 +10,15 @@ export default {
StageColumnComponent, StageColumnComponent,
LoadingIcon, LoadingIcon,
}, },
=======
export default {
components: {
StageColumnComponent,
LoadingIcon,
},
>>>>>>> upstream/master
props: { props: {
isLoading: { isLoading: {
type: Boolean, type: Boolean,
...@@ -28,6 +38,7 @@ export default { ...@@ -28,6 +38,7 @@ export default {
computed: { computed: {
graph() { graph() {
return this.pipeline.details && this.pipeline.details.stages; return this.pipeline.details && this.pipeline.details.stages;
<<<<<<< HEAD
}, },
triggered() { triggered() {
return this.pipeline.triggered || []; return this.pipeline.triggered || [];
...@@ -42,6 +53,9 @@ export default { ...@@ -42,6 +53,9 @@ export default {
hasTriggeredBy() { hasTriggeredBy() {
return !!this.triggeredBy.length; return !!this.triggeredBy.length;
}, },
=======
},
>>>>>>> upstream/master
}, },
methods: { methods: {
...@@ -106,7 +120,10 @@ export default { ...@@ -106,7 +120,10 @@ export default {
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:action-disabled="actionDisabled" :action-disabled="actionDisabled"
<<<<<<< HEAD
:has-triggered-by="hasTriggeredBy" :has-triggered-by="hasTriggeredBy"
=======
>>>>>>> upstream/master
/> />
</ul> </ul>
......
...@@ -28,8 +28,18 @@ export default { ...@@ -28,8 +28,18 @@ export default {
type: String, type: String,
required: false, required: false,
default: '', default: '',
<<<<<<< HEAD
=======
}, },
actionDisabled: {
type: String,
required: false,
default: null,
>>>>>>> upstream/master
},
},
<<<<<<< HEAD
actionDisabled: { actionDisabled: {
type: String, type: String,
required: false, required: false,
...@@ -46,6 +56,13 @@ export default { ...@@ -46,6 +56,13 @@ export default {
return list[0]; return list[0];
}, },
=======
methods: {
firstJob(list) {
return list[0];
},
>>>>>>> upstream/master
jobId(job) { jobId(job) {
return `ci-badge-${job.name}`; return `ci-badge-${job.name}`;
}, },
......
<script> <script>
import { viewerInformationForPath } from './lib/viewer_utils'; import { viewerInformationForPath } from './lib/viewer_utils';
import MarkdownViewer from './viewers/markdown_viewer.vue'; import MarkdownViewer from './viewers/markdown_viewer.vue';
import ImageViewer from './viewers/image_viewer.vue';
import DownloadViewer from './viewers/download_viewer.vue';
export default { export default {
props: { props: {
content: { content: {
type: String, type: String,
required: true, default: '',
}, },
path: { path: {
type: String, type: String,
required: true, required: true,
}, },
fileSize: {
type: Number,
required: false,
default: 0,
},
projectPath: { projectPath: {
type: String, type: String,
required: false, required: false,
...@@ -20,12 +27,18 @@ export default { ...@@ -20,12 +27,18 @@ export default {
}, },
computed: { computed: {
viewer() { viewer() {
if (!this.path) return null;
const previewInfo = viewerInformationForPath(this.path); const previewInfo = viewerInformationForPath(this.path);
if (!previewInfo) return DownloadViewer;
switch (previewInfo.id) { switch (previewInfo.id) {
case 'markdown': case 'markdown':
return MarkdownViewer; return MarkdownViewer;
case 'image':
return ImageViewer;
default: default:
return null; return DownloadViewer;
} }
}, },
}, },
...@@ -36,6 +49,8 @@ export default { ...@@ -36,6 +49,8 @@ export default {
<div class="preview-container"> <div class="preview-container">
<component <component
:is="viewer" :is="viewer"
:path="path"
:file-size="fileSize"
:project-path="projectPath" :project-path="projectPath"
:content="content" :content="content"
/> />
......
const viewers = { const viewers = {
image: {
id: 'image',
},
markdown: { markdown: {
id: 'markdown', id: 'markdown',
previewTitle: 'Preview Markdown', previewTitle: 'Preview Markdown',
...@@ -7,6 +10,12 @@ const viewers = { ...@@ -7,6 +10,12 @@ const viewers = {
const fileNameViewers = {}; const fileNameViewers = {};
const fileExtensionViewers = { const fileExtensionViewers = {
jpg: 'image',
jpeg: 'image',
gif: 'image',
png: 'image',
bmp: 'image',
ico: 'image',
md: 'markdown', md: 'markdown',
markdown: 'markdown', markdown: 'markdown',
}; };
......
<script>
import Icon from '../../icon.vue';
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
components: {
Icon,
},
props: {
path: {
type: String,
required: true,
},
fileSize: {
type: Number,
required: false,
default: 0,
},
},
computed: {
fileSizeReadable() {
return numberToHumanSize(this.fileSize);
},
fileName() {
return this.path.split('/').pop();
},
},
};
</script>
<template>
<div class="file-container">
<div class="file-content">
<p class="prepend-top-10 file-info">
{{ fileName }} ({{ fileSizeReadable }})
</p>
<a
:href="path"
class="btn btn-default"
rel="nofollow"
download
target="_blank">
<icon
name="download"
css-classes="pull-left append-right-8"
:size="16"
/>
{{ __('Download') }}
</a>
</div>
</div>
</template>
<script>
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
props: {
path: {
type: String,
required: true,
},
fileSize: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
width: 0,
height: 0,
isZoomable: false,
isZoomed: false,
};
},
computed: {
fileSizeReadable() {
return numberToHumanSize(this.fileSize);
},
},
methods: {
onImgLoad() {
const contentImg = this.$refs.contentImg;
this.isZoomable =
contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height;
this.width = contentImg.naturalWidth;
this.height = contentImg.naturalHeight;
},
onImgClick() {
if (this.isZoomable) this.isZoomed = !this.isZoomed;
},
},
};
</script>
<template>
<div class="file-container">
<div class="file-content image_file">
<img
ref="contentImg"
:class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }"
:src="path"
:alt="path"
@load="onImgLoad"
@click="onImgClick"/>
<p class="file-info prepend-top-10">
<template v-if="fileSize>0">
{{ fileSizeReadable }}
</template>
<template v-if="fileSize>0 && width && height">
-
</template>
<template v-if="width && height">
{{ width }} x {{ height }}
</template>
</p>
</div>
</div>
</template>
...@@ -578,6 +578,7 @@ ...@@ -578,6 +578,7 @@
.prometheus-table-row-highlight { .prometheus-table-row-highlight {
background-color: $prometheus-table-row-highlight-color; background-color: $prometheus-table-row-highlight-color;
} }
<<<<<<< HEAD
// EE-only // EE-only
.cluster-health-graphs { .cluster-health-graphs {
...@@ -592,3 +593,5 @@ ...@@ -592,3 +593,5 @@
} }
} }
} }
=======
>>>>>>> upstream/master
...@@ -524,17 +524,17 @@ ...@@ -524,17 +524,17 @@
svg { svg {
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
position: relative; position: relative;
left: 5px; left: 1px;
top: 2px; top: -1px;
width: 18px; width: 16px;
height: 18px; height: 16px;
} }
&.play { &.play {
svg { svg {
width: #{$ci-action-icon-size - 8}; width: 16px;
height: #{$ci-action-icon-size - 8}; height: 16px;
left: 8px; left: 3px;
} }
} }
} }
......
...@@ -312,6 +312,45 @@ ...@@ -312,6 +312,45 @@
height: 100%; height: 100%;
overflow: auto; overflow: auto;
.file-container {
background-color: $gray-darker;
display: flex;
height: 100%;
align-items: center;
justify-content: center;
text-align: center;
.file-content {
padding: $gl-padding;
max-width: 100%;
max-height: 100%;
img {
max-width: 90%;
max-height: 90%;
}
.isZoomable {
cursor: pointer;
cursor: zoom-in;
&.isZoomed {
cursor: pointer;
cursor: zoom-out;
max-width: none;
max-height: none;
margin-right: $gl-padding;
}
}
}
.file-info {
font-size: $label-font-size;
color: $diff-image-info-color;
}
}
.md-previewer { .md-previewer {
padding: $gl-padding; padding: $gl-padding;
} }
......
...@@ -30,6 +30,8 @@ class Commit ...@@ -30,6 +30,8 @@ class Commit
MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH
COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
# Used by GFM to match and present link extensions on node texts and hrefs.
LINK_EXTENSION_PATTERN = /(patch)/.freeze
def banzai_render_context(field) def banzai_render_context(field)
pipeline = field == :description ? :commit_description : :single_line pipeline = field == :description ? :commit_description : :single_line
...@@ -143,7 +145,8 @@ class Commit ...@@ -143,7 +145,8 @@ class Commit
end end
def self.link_reference_pattern def self.link_reference_pattern
@link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/) @link_reference_pattern ||=
super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})?(\.(?<extension>#{LINK_EXTENSION_PATTERN}))?/)
end end
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
......
- illustration = local_assigns.fetch(:illustration) - illustration = local_assigns.fetch(:illustration)
- illustration_size = local_assigns.fetch(:illustration_size) - illustration_size = local_assigns.fetch(:illustration_size)
- title = local_assigns.fetch(:title) - title = local_assigns.fetch(:title)
- content = local_assigns.fetch(:content) - content = local_assigns.fetch(:content, nil)
- action = local_assigns.fetch(:action, nil) - action = local_assigns.fetch(:action, nil)
.row.empty-state .row.empty-state
...@@ -11,7 +11,8 @@ ...@@ -11,7 +11,8 @@
.col-xs-12 .col-xs-12
.text-content .text-content
%h4.text-center= title %h4.text-center= title
%p= content - if content
%p= content
- if action - if action
.text-center .text-center
= action = action
- detailed_status = @build.detailed_status(current_user)
- illustration = detailed_status.illustration
= render 'empty_state',
illustration: illustration[:image],
illustration_size: illustration[:size],
title: illustration[:title],
content: illustration[:content],
action: detailed_status.has_action? ? link_to(detailed_status.action_button_title, detailed_status.action_path, method: detailed_status.action_method, class: 'btn btn-primary', title: detailed_status.action_button_title) : nil
...@@ -56,7 +56,8 @@ ...@@ -56,7 +56,8 @@
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else - else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)} Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- if @build.started?
- if @build.has_trace?
.build-trace-container.prepend-top-default .build-trace-container.prepend-top-default
.top-bar.js-top-bar .top-bar.js-top-bar
.js-truncated-info.truncated-info.hidden-xs.pull-left.hidden< .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
...@@ -90,25 +91,9 @@ ...@@ -90,25 +91,9 @@
%pre.build-trace#build-trace %pre.build-trace#build-trace
%code.bash.js-build-output %code.bash.js-build-output
.build-loader-animation.js-build-refresh .build-loader-animation.js-build-refresh
- elsif @build.playable?
= render 'empty_state',
illustration: 'illustrations/manual_action.svg',
illustration_size: 'svg-394',
title: _('This job requires a manual action'),
content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments'),
action: ( link_to _('Trigger this manual action'), play_project_job_path(@project, @build), method: :post, class: 'btn btn-primary', title: _('Trigger this manual action') )
- elsif @build.created?
= render 'empty_state',
illustration: 'illustrations/job_not_triggered.svg',
illustration_size: 'svg-306',
title: _('This job has not been triggered yet'),
content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered')
- else - else
= render 'empty_state', = render "empty_states"
illustration: 'illustrations/pending_job_empty.svg',
illustration_size: 'svg-430',
title: _('This job has not started yet'),
content: _('This job is in pending state and is waiting to be picked by a runner')
= render "sidebar", builds: @builds = render "sidebar", builds: @builds
.js-build-options{ data: javascript_build_options } .js-build-options{ data: javascript_build_options }
......
---
title: Add support for patch link extension for commit links on GitLab Flavored Markdown
merge_request:
author:
type: added
---
title: Improve empty state for canceled job
merge_request: 17646
author:
type: fixed
---
title: Add Total CPU/Memory consumption metrics for Kubernetes
merge_request: 17731
author:
type: added
---
title: Fix 500 error when a merge request from a fork has conflicts and has not yet
been updated
merge_request:
author:
type: fixed
---
title: Automatically cleanup stale worktrees and lock files upon a push
merge_request:
author:
type: fixed
...@@ -157,12 +157,17 @@ ...@@ -157,12 +157,17 @@
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)' - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)'
label: Total label: Total
unit: "cores" unit: "cores"
<<<<<<< HEAD
- title: "Memory Usage (Pod Average)" - title: "Memory Usage (Pod Average)"
=======
- title: "Memory Usage (Pod average)"
>>>>>>> upstream/master
y_label: "Memory Used per Pod" y_label: "Memory Used per Pod"
required_metrics: required_metrics:
- container_memory_usage_bytes - container_memory_usage_bytes
weight: 2 weight: 2
queries: queries:
<<<<<<< HEAD
- query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024' - query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Pod average label: Pod average
unit: MB unit: MB
...@@ -171,11 +176,18 @@ ...@@ -171,11 +176,18 @@
unit: MB unit: MB
track: canary track: canary
- title: "Core Usage (Pod Average)" - title: "Core Usage (Pod Average)"
=======
- query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Pod average
unit: MB
- title: "Core Usage (Pod average)"
>>>>>>> upstream/master
y_label: "Cores per Pod" y_label: "Cores per Pod"
required_metrics: required_metrics:
- container_cpu_usage_seconds_total - container_cpu_usage_seconds_total
weight: 1 weight: 1
queries: queries:
<<<<<<< HEAD
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))' - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Pod average label: Pod average
unit: "cores" unit: "cores"
...@@ -184,3 +196,8 @@ ...@@ -184,3 +196,8 @@
unit: "cores" unit: "cores"
track: canary track: canary
=======
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Pod average
unit: "cores"
>>>>>>> upstream/master
...@@ -11,7 +11,7 @@ module SharedBuilds ...@@ -11,7 +11,7 @@ module SharedBuilds
step 'project has a recent build' do step 'project has a recent build' do
@pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
@build = create(:ci_build, :running, :coverage, pipeline: @pipeline) @build = create(:ci_build, :running, :coverage, :trace_artifact, pipeline: @pipeline)
end end
step 'recent build is successful' do step 'recent build is successful' do
......
...@@ -215,6 +215,10 @@ module Banzai ...@@ -215,6 +215,10 @@ module Banzai
extras << "comment #{$1}" extras << "comment #{$1}"
end end
extension = matches[:extension] if matches.names.include?("extension")
extras << extension if extension
extras extras
end end
......
...@@ -23,6 +23,10 @@ module Gitlab ...@@ -23,6 +23,10 @@ module Gitlab
'Cancel' 'Cancel'
end end
def action_button_title
_('Cancel this job')
end
def self.matches?(build, user) def self.matches?(build, user)
build.cancelable? build.cancelable?
end end
......
module Gitlab
module Ci
module Status
module Build
class Canceled < Status::Extended
def illustration
{
image: 'illustrations/canceled-job_empty.svg',
size: 'svg-430',
title: _('This job has been canceled')
}
end
def self.matches?(build, user)
build.canceled?
end
end
end
end
end
end
module Gitlab
module Ci
module Status
module Build
class Created < Status::Extended
def illustration
{
image: 'illustrations/job_not_triggered.svg',
size: 'svg-306',
title: _('This job has not been triggered yet'),
content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered')
}
end
def self.matches?(build, user)
build.created?
end
end
end
end
end
end
module Gitlab
module Ci
module Status
module Build
class Erased < Status::Extended
def illustration
{
image: 'illustrations/skipped-job_empty.svg',
size: 'svg-430',
title: _('Job has been erased')
}
end
def self.matches?(build, user)
build.erased?
end
end
end
end
end
end
...@@ -4,7 +4,13 @@ module Gitlab ...@@ -4,7 +4,13 @@ module Gitlab
module Build module Build
class Factory < Status::Factory class Factory < Status::Factory
def self.extended_statuses def self.extended_statuses
[[Status::Build::Cancelable, [[Status::Build::Erased,
Status::Build::Manual,
Status::Build::Canceled,
Status::Build::Created,
Status::Build::Pending,
Status::Build::Skipped],
[Status::Build::Cancelable,
Status::Build::Retryable], Status::Build::Retryable],
[Status::Build::Failed], [Status::Build::Failed],
[Status::Build::FailedAllowed, [Status::Build::FailedAllowed,
......
module Gitlab
module Ci
module Status
module Build
class Manual < Status::Extended
def illustration
{
image: 'illustrations/manual_action.svg',
size: 'svg-394',
title: _('This job requires a manual action'),
content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments')
}
end
def self.matches?(build, user)
build.playable?
end
end
end
end
end
end
module Gitlab
module Ci
module Status
module Build
class Pending < Status::Extended
def illustration
{
image: 'illustrations/pending_job_empty.svg',
size: 'svg-430',
title: _('This job has not started yet'),
content: _('This job is in pending state and is waiting to be picked by a runner')
}
end
def self.matches?(build, user)
build.pending?
end
end
end
end
end
end
...@@ -19,6 +19,10 @@ module Gitlab ...@@ -19,6 +19,10 @@ module Gitlab
'Play' 'Play'
end end
def action_button_title
_('Trigger this manual action')
end
def action_path def action_path
play_project_job_path(subject.project, subject) play_project_job_path(subject.project, subject)
end end
......
...@@ -15,6 +15,10 @@ module Gitlab ...@@ -15,6 +15,10 @@ module Gitlab
'Retry' 'Retry'
end end
def action_button_title
_('Retry this job')
end
def action_path def action_path
retry_project_job_path(subject.project, subject) retry_project_job_path(subject.project, subject)
end end
......
module Gitlab
module Ci
module Status
module Build
class Skipped < Status::Extended
def illustration
{
image: 'illustrations/skipped-job_empty.svg',
size: 'svg-430',
title: _('This job has been skipped')
}
end
def self.matches?(build, user)
build.skipped?
end
end
end
end
end
end
...@@ -19,6 +19,10 @@ module Gitlab ...@@ -19,6 +19,10 @@ module Gitlab
'Stop' 'Stop'
end end
def action_button_title
_('Stop this environment')
end
def action_path def action_path
play_project_job_path(subject.project, subject) play_project_job_path(subject.project, subject)
end end
......
...@@ -22,6 +22,10 @@ module Gitlab ...@@ -22,6 +22,10 @@ module Gitlab
raise NotImplementedError raise NotImplementedError
end end
def illustration
raise NotImplementedError
end
def label def label
raise NotImplementedError raise NotImplementedError
end end
...@@ -58,6 +62,10 @@ module Gitlab ...@@ -58,6 +62,10 @@ module Gitlab
raise NotImplementedError raise NotImplementedError
end end
def action_button_title
raise NotImplementedError
end
# Hint that appears on all the pipeline graph tooltips and builds on the right sidebar in Job detail view # Hint that appears on all the pipeline graph tooltips and builds on the right sidebar in Job detail view
def status_tooltip def status_tooltip
label label
......
...@@ -23,7 +23,7 @@ module Gitlab ...@@ -23,7 +23,7 @@ module Gitlab
end end
rescue GRPC::FailedPrecondition => e rescue GRPC::FailedPrecondition => e
raise Gitlab::Git::Conflict::Resolver::ConflictSideMissing.new(e.message) raise Gitlab::Git::Conflict::Resolver::ConflictSideMissing.new(e.message)
rescue Rugged::OdbError, GRPC::BadStatus => e rescue Rugged::ReferenceError, Rugged::OdbError, GRPC::BadStatus => e
raise Gitlab::Git::CommandError.new(e) raise Gitlab::Git::CommandError.new(e)
end end
......
...@@ -1375,6 +1375,18 @@ module Gitlab ...@@ -1375,6 +1375,18 @@ module Gitlab
raise CommandError.new(e) raise CommandError.new(e)
end end
def clean_stale_repository_files
gitaly_migrate(:repository_cleanup, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
gitaly_repository_client.cleanup if is_enabled && exists?
end
rescue Gitlab::Git::CommandError => e # Don't fail if we can't cleanup
Rails.logger.error("Unable to clean repository on storage #{storage} with path #{path}: #{e.message}")
Gitlab::Metrics.counter(
:failed_repository_cleanup_total,
'Number of failed repository cleanup events'
).increment
end
def branch_names_contains_sha(sha) def branch_names_contains_sha(sha)
gitaly_migrate(:branch_names_contains_sha) do |is_enabled| gitaly_migrate(:branch_names_contains_sha) do |is_enabled|
if is_enabled if is_enabled
...@@ -1479,6 +1491,36 @@ module Gitlab ...@@ -1479,6 +1491,36 @@ module Gitlab
run_git!(['rev-list', '--max-count=1', oldrev, "^#{newrev}"]) run_git!(['rev-list', '--max-count=1', oldrev, "^#{newrev}"])
end end
<<<<<<< HEAD
=======
def with_worktree(worktree_path, branch, sparse_checkout_files: nil, env:)
base_args = %w(worktree add --detach)
# Note that we _don't_ want to test for `.present?` here: If the caller
# passes an non nil empty value it means it still wants sparse checkout
# but just isn't interested in any file, perhaps because it wants to
# checkout files in by a changeset but that changeset only adds files.
if sparse_checkout_files
# Create worktree without checking out
run_git!(base_args + ['--no-checkout', worktree_path], env: env)
worktree_git_path = run_git!(%w(rev-parse --git-dir), chdir: worktree_path).chomp
configure_sparse_checkout(worktree_git_path, sparse_checkout_files)
# After sparse checkout configuration, checkout `branch` in worktree
run_git!(%W(checkout --detach #{branch}), chdir: worktree_path, env: env)
else
# Create worktree and checkout `branch` in it
run_git!(base_args + [worktree_path, branch], env: env)
end
yield
ensure
FileUtils.rm_rf(worktree_path) if File.exist?(worktree_path)
FileUtils.rm_rf(worktree_git_path) if worktree_git_path && File.exist?(worktree_git_path)
end
>>>>>>> upstream/master
def checksum def checksum
gitaly_migrate(:calculate_checksum) do |is_enabled| gitaly_migrate(:calculate_checksum) do |is_enabled|
if is_enabled if is_enabled
...@@ -1575,33 +1617,6 @@ module Gitlab ...@@ -1575,33 +1617,6 @@ module Gitlab
File.exist?(path) && !clean_stuck_worktree(path) File.exist?(path) && !clean_stuck_worktree(path)
end end
def with_worktree(worktree_path, branch, sparse_checkout_files: nil, env:)
base_args = %w(worktree add --detach)
# Note that we _don't_ want to test for `.present?` here: If the caller
# passes an non nil empty value it means it still wants sparse checkout
# but just isn't interested in any file, perhaps because it wants to
# checkout files in by a changeset but that changeset only adds files.
if sparse_checkout_files
# Create worktree without checking out
run_git!(base_args + ['--no-checkout', worktree_path], env: env)
worktree_git_path = run_git!(%w(rev-parse --git-dir), chdir: worktree_path).chomp
configure_sparse_checkout(worktree_git_path, sparse_checkout_files)
# After sparse checkout configuration, checkout `branch` in worktree
run_git!(%W(checkout --detach #{branch}), chdir: worktree_path, env: env)
else
# Create worktree and checkout `branch` in it
run_git!(base_args + [worktree_path, branch], env: env)
end
yield
ensure
FileUtils.rm_rf(worktree_path) if File.exist?(worktree_path)
FileUtils.rm_rf(worktree_git_path) if worktree_git_path && File.exist?(worktree_git_path)
end
def clean_stuck_worktree(path) def clean_stuck_worktree(path)
return false unless File.mtime(path) < 15.minutes.ago return false unless File.mtime(path) < 15.minutes.ago
......
...@@ -251,6 +251,11 @@ module Gitlab ...@@ -251,6 +251,11 @@ module Gitlab
end end
def check_change_access!(changes) def check_change_access!(changes)
# If there are worktrees with a HEAD pointing to a non-existent object,
# calls to `git rev-list --all` will fail in git 2.15+. This should also
# clear stale lock files.
project.repository.clean_stale_repository_files
changes_list = Gitlab::ChangesList.new(changes) changes_list = Gitlab::ChangesList.new(changes)
push_size_in_bytes = 0 push_size_in_bytes = 0
......
...@@ -19,6 +19,11 @@ module Gitlab ...@@ -19,6 +19,11 @@ module Gitlab
response.exists response.exists
end end
def cleanup
request = Gitaly::CleanupRequest.new(repository: @gitaly_repo)
GitalyClient.call(@storage, :repository_service, :cleanup, request)
end
def garbage_collect(create_bitmap) def garbage_collect(create_bitmap)
request = Gitaly::GarbageCollectRequest.new(repository: @gitaly_repo, create_bitmap: create_bitmap) request = Gitaly::GarbageCollectRequest.new(repository: @gitaly_repo, create_bitmap: create_bitmap)
GitalyClient.call(@storage, :repository_service, :garbage_collect, request) GitalyClient.call(@storage, :repository_service, :garbage_collect, request)
......
...@@ -9,6 +9,7 @@ describe 'Merge request < User sees mini pipeline graph', :js do ...@@ -9,6 +9,7 @@ describe 'Merge request < User sees mini pipeline graph', :js do
before do before do
build.run build.run
build.trace.set('hello')
sign_in(user) sign_in(user)
visit_merge_request visit_merge_request
end end
...@@ -26,15 +27,15 @@ describe 'Merge request < User sees mini pipeline graph', :js do ...@@ -26,15 +27,15 @@ describe 'Merge request < User sees mini pipeline graph', :js do
let(:artifacts_file2) { fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') } let(:artifacts_file2) { fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') }
before do before do
create(:ci_build, pipeline: pipeline, legacy_artifacts_file: artifacts_file1) create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file1)
create(:ci_build, pipeline: pipeline, when: 'manual') create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
end end
it 'avoids repeated database queries' do it 'avoids repeated database queries' do
before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
create(:ci_build, pipeline: pipeline, legacy_artifacts_file: artifacts_file2) create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file2)
create(:ci_build, pipeline: pipeline, when: 'manual') create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
......
require 'spec_helper' require 'spec_helper'
describe 'User browses a job', :js do describe 'User browses a job', :js do
let!(:build) { create(:ci_build, :running, :coverage, pipeline: pipeline) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user_access_level) { :developer }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
let!(:build) { create(:ci_build, :success, :trace_artifact, :coverage, pipeline: pipeline) }
before do before do
project.add_master(user) project.add_master(user)
project.enable_ci project.enable_ci
build.success
build.trace.set('job trace')
sign_in(user) sign_in(user)
...@@ -21,7 +20,9 @@ describe 'User browses a job', :js do ...@@ -21,7 +20,9 @@ describe 'User browses a job', :js do
expect(page).to have_content("Job ##{build.id}") expect(page).to have_content("Job ##{build.id}")
expect(page).to have_css('#build-trace') expect(page).to have_css('#build-trace')
accept_confirm { click_link('Erase') } # scroll to the top of the page first
execute_script "window.scrollTo(0,0)"
accept_confirm { find('.js-erase-link').click }
expect(page).to have_no_css('.artifacts') expect(page).to have_no_css('.artifacts')
expect(build).not_to have_trace expect(build).not_to have_trace
...@@ -36,7 +37,7 @@ describe 'User browses a job', :js do ...@@ -36,7 +37,7 @@ describe 'User browses a job', :js do
end end
context 'with a failed job' do context 'with a failed job' do
let!(:build) { create(:ci_build, :failed, pipeline: pipeline) } let!(:build) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) }
it 'displays the failure reason' do it 'displays the failure reason' do
within('.builds-container') do within('.builds-container') do
...@@ -47,7 +48,7 @@ describe 'User browses a job', :js do ...@@ -47,7 +48,7 @@ describe 'User browses a job', :js do
end end
context 'when a failed job has been retried' do context 'when a failed job has been retried' do
let!(:build) { create(:ci_build, :failed, :retried, pipeline: pipeline) } let!(:build) { create(:ci_build, :failed, :retried, :trace_artifact, pipeline: pipeline) }
it 'displays the failure reason and retried label' do it 'displays the failure reason and retried label' do
within('.builds-container') do within('.builds-container') do
......
...@@ -113,7 +113,7 @@ feature 'Jobs' do ...@@ -113,7 +113,7 @@ feature 'Jobs' do
describe "GET /:project/jobs/:id" do describe "GET /:project/jobs/:id" do
context "Job from project" do context "Job from project" do
let(:job) { create(:ci_build, :success, pipeline: pipeline) } let(:job) { create(:ci_build, :success, :trace_live, pipeline: pipeline) }
before do before do
visit project_job_path(project, job) visit project_job_path(project, job)
...@@ -136,7 +136,7 @@ feature 'Jobs' do ...@@ -136,7 +136,7 @@ feature 'Jobs' do
end end
context 'when job is not running', :js do context 'when job is not running', :js do
let(:job) { create(:ci_build, :success, pipeline: pipeline) } let(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
before do before do
visit project_job_path(project, job) visit project_job_path(project, job)
...@@ -153,7 +153,7 @@ feature 'Jobs' do ...@@ -153,7 +153,7 @@ feature 'Jobs' do
end end
context 'if job failed' do context 'if job failed' do
let(:job) { create(:ci_build, :failed, pipeline: pipeline) } let(:job) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) }
before do before do
visit project_job_path(project, job) visit project_job_path(project, job)
...@@ -339,7 +339,7 @@ feature 'Jobs' do ...@@ -339,7 +339,7 @@ feature 'Jobs' do
context 'job is successfull and has deployment' do context 'job is successfull and has deployment' do
let(:deployment) { create(:deployment) } let(:deployment) { create(:deployment) }
let(:job) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) } let(:job) { create(:ci_build, :success, :trace_artifact, environment: environment.name, deployments: [deployment], pipeline: pipeline) }
it 'shows a link for the job' do it 'shows a link for the job' do
visit project_job_path(project, job) visit project_job_path(project, job)
...@@ -349,7 +349,7 @@ feature 'Jobs' do ...@@ -349,7 +349,7 @@ feature 'Jobs' do
end end
context 'job is complete and not successful' do context 'job is complete and not successful' do
let(:job) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } let(:job) { create(:ci_build, :failed, :trace_artifact, environment: environment.name, pipeline: pipeline) }
it 'shows a link for the job' do it 'shows a link for the job' do
visit project_job_path(project, job) visit project_job_path(project, job)
...@@ -360,7 +360,7 @@ feature 'Jobs' do ...@@ -360,7 +360,7 @@ feature 'Jobs' do
context 'job creates a new deployment' do context 'job creates a new deployment' do
let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) }
let(:job) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } let(:job) { create(:ci_build, :success, :trace_artifact, environment: environment.name, pipeline: pipeline) }
it 'shows a link to latest deployment' do it 'shows a link to latest deployment' do
visit project_job_path(project, job) visit project_job_path(project, job)
...@@ -390,6 +390,7 @@ feature 'Jobs' do ...@@ -390,6 +390,7 @@ feature 'Jobs' do
end end
it 'shows manual action empty state' do it 'shows manual action empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).to have_content('This job requires a manual action') expect(page).to have_content('This job requires a manual action')
expect(page).to have_content('This job depends on a user to trigger its process. Often they are used to deploy code to production environments') expect(page).to have_content('This job depends on a user to trigger its process. Often they are used to deploy code to production environments')
expect(page).to have_link('Trigger this manual action') expect(page).to have_link('Trigger this manual action')
...@@ -413,6 +414,7 @@ feature 'Jobs' do ...@@ -413,6 +414,7 @@ feature 'Jobs' do
end end
it 'shows empty state' do it 'shows empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).to have_content('This job has not been triggered yet') expect(page).to have_content('This job has not been triggered yet')
expect(page).to have_content('This job depends on upstream jobs that need to succeed in order for this job to be triggered') expect(page).to have_content('This job depends on upstream jobs that need to succeed in order for this job to be triggered')
end end
...@@ -426,10 +428,53 @@ feature 'Jobs' do ...@@ -426,10 +428,53 @@ feature 'Jobs' do
end end
it 'shows pending empty state' do it 'shows pending empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).to have_content('This job has not started yet') expect(page).to have_content('This job has not started yet')
expect(page).to have_content('This job is in pending state and is waiting to be picked by a runner') expect(page).to have_content('This job is in pending state and is waiting to be picked by a runner')
end end
end end
context 'Canceled job' do
context 'with log' do
let(:job) { create(:ci_build, :canceled, :trace_artifact, pipeline: pipeline) }
before do
visit project_job_path(project, job)
end
it 'renders job log' do
expect(page).to have_selector('.js-build-output')
end
end
context 'without log' do
let(:job) { create(:ci_build, :canceled, pipeline: pipeline) }
before do
visit project_job_path(project, job)
end
it 'renders empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).not_to have_selector('.js-build-output')
expect(page).to have_content('This job has been canceled')
end
end
end
context 'Skipped job' do
let(:job) { create(:ci_build, :skipped, pipeline: pipeline) }
before do
visit project_job_path(project, job)
end
it 'renders empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).not_to have_selector('.js-build-output')
expect(page).to have_content('This job has been skipped')
end
end
end end
describe "POST /:project/jobs/:id/cancel", :js do describe "POST /:project/jobs/:id/cancel", :js do
......
...@@ -38,4 +38,33 @@ describe('ContentViewer', () => { ...@@ -38,4 +38,33 @@ describe('ContentViewer', () => {
done(); done();
}); });
}); });
it('renders image preview', done => {
createComponent({
path: 'test.jpg',
fileSize: 1024,
});
setTimeout(() => {
expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe('test.jpg');
done();
});
});
it('renders fallback download control', done => {
createComponent({
path: 'test.abc',
fileSize: 1024,
});
setTimeout(() => {
expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain(
'test.abc (1.00 KiB)',
);
expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toContain('Download');
done();
});
});
}); });
...@@ -207,4 +207,35 @@ describe Banzai::Filter::CommitReferenceFilter do ...@@ -207,4 +207,35 @@ describe Banzai::Filter::CommitReferenceFilter do
expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>}) expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>})
end end
end end
context 'URL reference for a commit patch' do
let(:namespace) { create(:namespace) }
let(:project2) { create(:project, :public, :repository, namespace: namespace) }
let(:commit) { project2.commit }
let(:link) { urls.project_commit_url(project2, commit.id) }
let(:extension) { '.patch' }
let(:reference) { link + extension }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href'))
.to eq reference
end
it 'has valid text' do
doc = reference_filter("See #{reference}")
expect(doc.text).to eq("See #{commit.reference_link_text(project)} (patch)")
end
it 'does not link to patch when extension match is after the path' do
invalidate_commit_reference = reference_filter("#{link}/builds.patch")
doc = reference_filter("See (#{invalidate_commit_reference})")
expect(doc.css('a').first.attr('href')).to eq "#{link}/builds"
expect(doc.text).to eq("See (#{commit.reference_link_text(project)} (builds).patch)")
end
end
end end
...@@ -90,6 +90,10 @@ describe Gitlab::Ci::Status::Build::Cancelable do ...@@ -90,6 +90,10 @@ describe Gitlab::Ci::Status::Build::Cancelable do
describe '#action_title' do describe '#action_title' do
it { expect(subject.action_title).to eq 'Cancel' } it { expect(subject.action_title).to eq 'Cancel' }
end end
describe '#action_button_title' do
it { expect(subject.action_button_title).to eq 'Cancel this job' }
end
end end
describe '.matches?' do describe '.matches?' do
......
require 'spec_helper'
describe Gitlab::Ci::Status::Build::Canceled do
let(:user) { create(:user) }
subject do
described_class.new(double('subject'))
end
describe '#illustration' do
it { expect(subject.illustration).to include(:image, :size, :title) }
end
describe '.matches?' do
subject {described_class.matches?(build, user) }
context 'when build is canceled' do
let(:build) { create(:ci_build, :canceled) }
it 'is a correct match' do
expect(subject).to be true
end
end
context 'when build is not canceled' do
let(:build) { create(:ci_build) }
it 'does not match' do
expect(subject).to be false
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Status::Build::Created do
let(:user) { create(:user) }
subject do
described_class.new(double('subject'))
end
describe '#illustration' do
it { expect(subject.illustration).to include(:image, :size, :title, :content) }
end
describe '.matches?' do
subject {described_class.matches?(build, user) }
context 'when build is created' do
let(:build) { create(:ci_build, :created) }
it 'is a correct match' do
expect(subject).to be true
end
end
context 'when build is not created' do
let(:build) { create(:ci_build) }
it 'does not match' do
expect(subject).to be false
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Status::Build::Erased do
let(:user) { create(:user) }
subject do
described_class.new(double('subject'))
end
describe '#illustration' do
it { expect(subject.illustration).to include(:image, :size, :title) }
end
describe '.matches?' do
subject { described_class.matches?(build, user) }
context 'when build is erased' do
let(:build) { create(:ci_build, :success, :erased) }
it 'is a correct match' do
expect(subject).to be true
end
end
context 'when build is not erased' do
let(:build) { create(:ci_build, :success, :trace_artifact) }
it 'does not match' do
expect(subject).to be false
end
end
end
end
...@@ -13,7 +13,7 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -13,7 +13,7 @@ describe Gitlab::Ci::Status::Build::Factory do
end end
context 'when build is successful' do context 'when build is successful' do
let(:build) { create(:ci_build, :success) } let(:build) { create(:ci_build, :success, :trace_artifact) }
it 'matches correct core status' do it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Success expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
...@@ -38,6 +38,33 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -38,6 +38,33 @@ describe Gitlab::Ci::Status::Build::Factory do
end end
end end
context 'when build is erased' do
let(:build) { create(:ci_build, :success, :erased) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Erased,
Gitlab::Ci::Status::Build::Retryable]
end
it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end
it 'fabricates status with correct details' do
expect(status.text).to eq 'passed'
expect(status.icon).to eq 'status_success'
expect(status.favicon).to eq 'favicon_status_success'
expect(status.label).to eq 'passed'
expect(status).to have_details
expect(status).to have_action
end
end
context 'when build is failed' do context 'when build is failed' do
context 'when build is not allowed to fail' do context 'when build is not allowed to fail' do
let(:build) { create(:ci_build, :failed) } let(:build) { create(:ci_build, :failed) }
...@@ -106,7 +133,7 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -106,7 +133,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do it 'matches correct extended statuses' do
expect(factory.extended_statuses) expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Retryable] .to eq [Gitlab::Ci::Status::Build::Canceled, Gitlab::Ci::Status::Build::Retryable]
end end
it 'fabricates a retryable build status' do it 'fabricates a retryable build status' do
...@@ -117,6 +144,7 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -117,6 +144,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.text).to eq 'canceled' expect(status.text).to eq 'canceled'
expect(status.icon).to eq 'status_canceled' expect(status.icon).to eq 'status_canceled'
expect(status.favicon).to eq 'favicon_status_canceled' expect(status.favicon).to eq 'favicon_status_canceled'
expect(status.illustration).to include(:image, :size, :title)
expect(status.label).to eq 'canceled' expect(status.label).to eq 'canceled'
expect(status).to have_details expect(status).to have_details
expect(status).to have_action expect(status).to have_action
...@@ -158,7 +186,7 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -158,7 +186,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do it 'matches correct extended statuses' do
expect(factory.extended_statuses) expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Cancelable] .to eq [Gitlab::Ci::Status::Build::Pending, Gitlab::Ci::Status::Build::Cancelable]
end end
it 'fabricates a cancelable build status' do it 'fabricates a cancelable build status' do
...@@ -169,6 +197,7 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -169,6 +197,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.text).to eq 'pending' expect(status.text).to eq 'pending'
expect(status.icon).to eq 'status_pending' expect(status.icon).to eq 'status_pending'
expect(status.favicon).to eq 'favicon_status_pending' expect(status.favicon).to eq 'favicon_status_pending'
expect(status.illustration).to include(:image, :size, :title, :content)
expect(status.label).to eq 'pending' expect(status.label).to eq 'pending'
expect(status).to have_details expect(status).to have_details
expect(status).to have_action expect(status).to have_action
...@@ -182,18 +211,19 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -182,18 +211,19 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
end end
it 'does not match extended statuses' do it 'matches correct extended statuses' do
expect(factory.extended_statuses).to be_empty expect(factory.extended_statuses).to eq [Gitlab::Ci::Status::Build::Skipped]
end end
it 'fabricates a core skipped status' do it 'fabricates a skipped build status' do
expect(status).to be_a Gitlab::Ci::Status::Skipped expect(status).to be_a Gitlab::Ci::Status::Build::Skipped
end end
it 'fabricates status with correct details' do it 'fabricates status with correct details' do
expect(status.text).to eq 'skipped' expect(status.text).to eq 'skipped'
expect(status.icon).to eq 'status_skipped' expect(status.icon).to eq 'status_skipped'
expect(status.favicon).to eq 'favicon_status_skipped' expect(status.favicon).to eq 'favicon_status_skipped'
expect(status.illustration).to include(:image, :size, :title)
expect(status.label).to eq 'skipped' expect(status.label).to eq 'skipped'
expect(status).to have_details expect(status).to have_details
expect(status).not_to have_action expect(status).not_to have_action
...@@ -210,7 +240,8 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -210,7 +240,8 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do it 'matches correct extended statuses' do
expect(factory.extended_statuses) expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Play, .to eq [Gitlab::Ci::Status::Build::Manual,
Gitlab::Ci::Status::Build::Play,
Gitlab::Ci::Status::Build::Action] Gitlab::Ci::Status::Build::Action]
end end
...@@ -223,6 +254,7 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -223,6 +254,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.group).to eq 'manual' expect(status.group).to eq 'manual'
expect(status.icon).to eq 'status_manual' expect(status.icon).to eq 'status_manual'
expect(status.favicon).to eq 'favicon_status_manual' expect(status.favicon).to eq 'favicon_status_manual'
expect(status.illustration).to include(:image, :size, :title, :content)
expect(status.label).to include 'manual play action' expect(status.label).to include 'manual play action'
expect(status).to have_details expect(status).to have_details
expect(status.action_path).to include 'play' expect(status.action_path).to include 'play'
...@@ -257,7 +289,8 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -257,7 +289,8 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do it 'matches correct extended statuses' do
expect(factory.extended_statuses) expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Stop, .to eq [Gitlab::Ci::Status::Build::Manual,
Gitlab::Ci::Status::Build::Stop,
Gitlab::Ci::Status::Build::Action] Gitlab::Ci::Status::Build::Action]
end end
......
require 'spec_helper'
describe Gitlab::Ci::Status::Build::Manual do
let(:user) { create(:user) }
subject do
build = create(:ci_build, :manual)
described_class.new(Gitlab::Ci::Status::Core.new(build, user))
end
describe '#illustration' do
it { expect(subject.illustration).to include(:image, :size, :title, :content) }
end
describe '.matches?' do
subject {described_class.matches?(build, user) }
context 'when build is manual' do
let(:build) { create(:ci_build, :manual) }
it 'is a correct match' do
expect(subject).to be true
end
end
context 'when build is not manual' do
let(:build) { create(:ci_build) }
it 'does not match' do
expect(subject).to be false
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Status::Build::Pending do
let(:user) { create(:user) }
subject do
described_class.new(double('subject'))
end
describe '#illustration' do
it { expect(subject.illustration).to include(:image, :size, :title, :content) }
end
describe '.matches?' do
subject {described_class.matches?(build, user) }
context 'when build is pending' do
let(:build) { create(:ci_build, :pending) }
it 'is a correct match' do
expect(subject).to be true
end
end
context 'when build is not pending' do
let(:build) { create(:ci_build, :success) }
it 'does not match' do
expect(subject).to be false
end
end
end
end
...@@ -69,6 +69,10 @@ describe Gitlab::Ci::Status::Build::Play do ...@@ -69,6 +69,10 @@ describe Gitlab::Ci::Status::Build::Play do
it { expect(subject.action_title).to eq 'Play' } it { expect(subject.action_title).to eq 'Play' }
end end
describe '#action_button_title' do
it { expect(subject.action_button_title).to eq 'Trigger this manual action' }
end
describe '.matches?' do describe '.matches?' do
subject { described_class.matches?(build, user) } subject { described_class.matches?(build, user) }
......
...@@ -90,6 +90,10 @@ describe Gitlab::Ci::Status::Build::Retryable do ...@@ -90,6 +90,10 @@ describe Gitlab::Ci::Status::Build::Retryable do
describe '#action_title' do describe '#action_title' do
it { expect(subject.action_title).to eq 'Retry' } it { expect(subject.action_title).to eq 'Retry' }
end end
describe '#action_button_title' do
it { expect(subject.action_button_title).to eq 'Retry this job' }
end
end end
describe '.matches?' do describe '.matches?' do
......
require 'spec_helper'
describe Gitlab::Ci::Status::Build::Skipped do
let(:user) { create(:user) }
subject do
described_class.new(double('subject'))
end
describe '#illustration' do
it { expect(subject.illustration).to include(:image, :size, :title) }
end
describe '.matches?' do
subject {described_class.matches?(build, user) }
context 'when build is skipped' do
let(:build) { create(:ci_build, :skipped) }
it 'is a correct match' do
expect(subject).to be true
end
end
context 'when build is not skipped' do
let(:build) { create(:ci_build) }
it 'does not match' do
expect(subject).to be false
end
end
end
end
...@@ -44,6 +44,10 @@ describe Gitlab::Ci::Status::Build::Stop do ...@@ -44,6 +44,10 @@ describe Gitlab::Ci::Status::Build::Stop do
describe '#action_title' do describe '#action_title' do
it { expect(subject.action_title).to eq 'Stop' } it { expect(subject.action_title).to eq 'Stop' }
end end
describe '#action_button_title' do
it { expect(subject.action_button_title).to eq 'Stop this environment' }
end
end end
describe '.matches?' do describe '.matches?' do
......
...@@ -2306,6 +2306,39 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -2306,6 +2306,39 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
end end
describe '#clean_stale_repository_files' do
let(:worktree_path) { File.join(repository.path, 'worktrees', 'delete-me') }
it 'cleans up the files' do
repository.with_worktree(worktree_path, 'master', env: ENV) do
FileUtils.touch(worktree_path, mtime: Time.now - 8.hours)
# git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object,
# but the HEAD must be 40 characters long or git will ignore it.
File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA)
# git 2.16 fails with "fatal: bad object HEAD"
expect { repository.rev_list(including: :all) }.to raise_error(Gitlab::Git::Repository::GitError)
repository.clean_stale_repository_files
expect { repository.rev_list(including: :all) }.not_to raise_error
expect(File.exist?(worktree_path)).to be_falsey
end
end
it 'increments a counter upon an error' do
expect(repository.gitaly_repository_client).to receive(:cleanup).and_raise(Gitlab::Git::CommandError)
counter = double(:counter)
expect(counter).to receive(:increment)
expect(Gitlab::Metrics).to receive(:counter).with(:failed_repository_cleanup_total,
'Number of failed repository cleanup events').and_return(counter)
repository.clean_stale_repository_files
end
end
describe '#delete_remote_branches' do describe '#delete_remote_branches' do
subject do subject do
repository.delete_remote_branches('downstream-remote', ['master']) repository.delete_remote_branches('downstream-remote', ['master'])
......
...@@ -977,6 +977,7 @@ describe Gitlab::GitAccess do ...@@ -977,6 +977,7 @@ describe Gitlab::GitAccess do
end end
end end
<<<<<<< HEAD
describe "push_rule_check" do describe "push_rule_check" do
let(:start_sha) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } let(:start_sha) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' }
let(:end_sha) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' } let(:end_sha) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' }
...@@ -1126,6 +1127,19 @@ describe Gitlab::GitAccess do ...@@ -1126,6 +1127,19 @@ describe Gitlab::GitAccess do
expect { access.send(:check_push_access!, "#{start_sha} #{end_sha} refs/heads/master") }.not_to raise_error expect { access.send(:check_push_access!, "#{start_sha} #{end_sha} refs/heads/master") }.not_to raise_error
end end
=======
context 'when pushing to a project' do
let(:project) { create(:project, :public, :repository) }
let(:changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow" }
before do
project.add_developer(user)
end
it 'cleans up the files' do
expect(project.repository).to receive(:clean_stale_repository_files).and_call_original
expect { push_access_check }.not_to raise_error
>>>>>>> upstream/master
end end
end end
end end
......
...@@ -17,6 +17,16 @@ describe Gitlab::GitalyClient::RepositoryService do ...@@ -17,6 +17,16 @@ describe Gitlab::GitalyClient::RepositoryService do
end end
end end
describe '#cleanup' do
it 'sends a cleanup message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:cleanup)
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
client.cleanup
end
end
describe '#garbage_collect' do describe '#garbage_collect' do
it 'sends a garbage_collect message' do it 'sends a garbage_collect message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub) expect_any_instance_of(Gitaly::RepositoryService::Stub)
......
...@@ -40,8 +40,12 @@ describe RemoveSoftRemovedObjects, :migration do ...@@ -40,8 +40,12 @@ describe RemoveSoftRemovedObjects, :migration do
it 'removes routes of soft removed personal namespaces' do it 'removes routes of soft removed personal namespaces' do
namespace = create_with_deleted_at(:namespace) namespace = create_with_deleted_at(:namespace)
<<<<<<< HEAD
group = groups.create!(name: 'group', path: 'group_path', type: 'Group') group = groups.create!(name: 'group', path: 'group_path', type: 'Group')
routes.create!(source_id: group.id, source_type: 'Group', name: 'group', path: 'group_path') routes.create!(source_id: group.id, source_type: 'Group', name: 'group', path: 'group_path')
=======
group = create(:group) # rubocop:disable RSpec/FactoriesInMigrationSpecs
>>>>>>> upstream/master
expect(routes.where(source_id: namespace.id).exists?).to eq(true) expect(routes.where(source_id: namespace.id).exists?).to eq(true)
expect(routes.where(source_id: group.id).exists?).to eq(true) expect(routes.where(source_id: group.id).exists?).to eq(true)
...@@ -53,7 +57,11 @@ describe RemoveSoftRemovedObjects, :migration do ...@@ -53,7 +57,11 @@ describe RemoveSoftRemovedObjects, :migration do
end end
it 'schedules the removal of soft removed groups' do it 'schedules the removal of soft removed groups' do
<<<<<<< HEAD
group = create_deleted_group group = create_deleted_group
=======
group = create_with_deleted_at(:group)
>>>>>>> upstream/master
admin = create(:user, admin: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs admin = create(:user, admin: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs
expect_any_instance_of(GroupDestroyWorker) expect_any_instance_of(GroupDestroyWorker)
......
...@@ -77,6 +77,14 @@ describe MergeRequests::Conflicts::ListService do ...@@ -77,6 +77,14 @@ describe MergeRequests::Conflicts::ListService do
expect(service.can_be_resolved_in_ui?).to be_falsey expect(service.can_be_resolved_in_ui?).to be_falsey
end end
it 'returns a falsey value when the MR has a missing revision after a force push' do
merge_request = create_merge_request('conflict-resolvable')
service = conflicts_service(merge_request)
allow(merge_request).to receive_message_chain(:target_branch_head, :raw, :id).and_return(Gitlab::Git::BLANK_SHA)
expect(service.can_be_resolved_in_ui?).to be_falsey
end
context 'with gitaly disabled', :skip_gitaly_mock do context 'with gitaly disabled', :skip_gitaly_mock do
it 'returns a falsey value when the MR has a missing ref after a force push' do it 'returns a falsey value when the MR has a missing ref after a force push' do
merge_request = create_merge_request('conflict-resolvable') merge_request = create_merge_request('conflict-resolvable')
...@@ -85,6 +93,14 @@ describe MergeRequests::Conflicts::ListService do ...@@ -85,6 +93,14 @@ describe MergeRequests::Conflicts::ListService do
expect(service.can_be_resolved_in_ui?).to be_falsey expect(service.can_be_resolved_in_ui?).to be_falsey
end end
it 'returns a falsey value when the MR has a missing revision after a force push' do
merge_request = create_merge_request('conflict-resolvable')
service = conflicts_service(merge_request)
allow(merge_request).to receive_message_chain(:target_branch_head, :raw, :id).and_return(Gitlab::Git::BLANK_SHA)
expect(service.can_be_resolved_in_ui?).to be_falsey
end
end end
end end
end end
...@@ -70,10 +70,13 @@ RSpec.configure do |config| ...@@ -70,10 +70,13 @@ RSpec.configure do |config|
config.include StubFeatureFlags config.include StubFeatureFlags
config.include StubENV config.include StubENV
config.include ExpectOffense config.include ExpectOffense
<<<<<<< HEAD
# EE only # EE only
config.include EE::LicenseHelpers config.include EE::LicenseHelpers
config.include Rails.application.routes.url_helpers, type: :routing config.include Rails.application.routes.url_helpers, type: :routing
=======
>>>>>>> upstream/master
config.infer_spec_type_from_file_location! config.infer_spec_type_from_file_location!
......
...@@ -21,7 +21,7 @@ describe 'projects/jobs/show' do ...@@ -21,7 +21,7 @@ describe 'projects/jobs/show' do
describe 'environment info in job view' do describe 'environment info in job view' do
context 'job with latest deployment' do context 'job with latest deployment' do
let(:build) do let(:build) do
create(:ci_build, :success, environment: 'staging') create(:ci_build, :success, :trace_artifact, environment: 'staging')
end end
before do before do
...@@ -40,11 +40,11 @@ describe 'projects/jobs/show' do ...@@ -40,11 +40,11 @@ describe 'projects/jobs/show' do
context 'job with outdated deployment' do context 'job with outdated deployment' do
let(:build) do let(:build) do
create(:ci_build, :success, environment: 'staging', pipeline: pipeline) create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline)
end end
let(:second_build) do let(:second_build) do
create(:ci_build, :success, environment: 'staging', pipeline: pipeline) create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline)
end end
let(:environment) do let(:environment) do
...@@ -70,7 +70,7 @@ describe 'projects/jobs/show' do ...@@ -70,7 +70,7 @@ describe 'projects/jobs/show' do
context 'job failed to deploy' do context 'job failed to deploy' do
let(:build) do let(:build) do
create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) create(:ci_build, :failed, :trace_artifact, environment: 'staging', pipeline: pipeline)
end end
let!(:environment) do let!(:environment) do
...@@ -88,7 +88,7 @@ describe 'projects/jobs/show' do ...@@ -88,7 +88,7 @@ describe 'projects/jobs/show' do
context 'job will deploy' do context 'job will deploy' do
let(:build) do let(:build) do
create(:ci_build, :running, environment: 'staging', pipeline: pipeline) create(:ci_build, :running, :trace_live, environment: 'staging', pipeline: pipeline)
end end
context 'when environment exists' do context 'when environment exists' do
...@@ -136,7 +136,7 @@ describe 'projects/jobs/show' do ...@@ -136,7 +136,7 @@ describe 'projects/jobs/show' do
context 'job that failed to deploy and environment has not been created' do context 'job that failed to deploy and environment has not been created' do
let(:build) do let(:build) do
create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) create(:ci_build, :failed, :trace_artifact, environment: 'staging', pipeline: pipeline)
end end
let!(:environment) do let!(:environment) do
...@@ -154,7 +154,7 @@ describe 'projects/jobs/show' do ...@@ -154,7 +154,7 @@ describe 'projects/jobs/show' do
context 'job that will deploy and environment has not been created' do context 'job that will deploy and environment has not been created' do
let(:build) do let(:build) do
create(:ci_build, :running, environment: 'staging', pipeline: pipeline) create(:ci_build, :running, :trace_live, environment: 'staging', pipeline: pipeline)
end end
let!(:environment) do let!(:environment) do
...@@ -174,8 +174,9 @@ describe 'projects/jobs/show' do ...@@ -174,8 +174,9 @@ describe 'projects/jobs/show' do
end end
context 'when job is running' do context 'when job is running' do
let(:build) { create(:ci_build, :trace_live, :running, pipeline: pipeline) }
before do before do
build.run!
render render
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment