Commit 4182fe0b authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-04-06' into 'master'

CE upstream - 2018-04-06 23:39 UTC

See merge request gitlab-org/gitlab-ee!5285
parents 5eab5bbd cecf041a
......@@ -59,6 +59,8 @@ linters:
# Reports when you define the same property twice in a single rule set.
DuplicateProperty:
enabled: true
ignore_consecutive:
- cursor
# Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks:
......
......@@ -36,6 +36,7 @@ export default {
>
<a
v-tooltip
v-if="!file.binary"
:href="file.blamePath"
:title="__('Blame')"
class="btn btn-xs btn-transparent blame"
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
icon,
export default {
components: {
icon,
},
directives: {
tooltip,
},
mixins: [timeAgoMixin],
props: {
file: {
type: Object,
required: true,
},
directives: {
tooltip,
},
mixins: [
timeAgoMixin,
],
props: {
file: {
type: Object,
required: true,
},
},
};
},
};
</script>
<template>
......@@ -50,7 +48,9 @@
<div class="text-right">
{{ file.eol }}
</div>
<div class="text-right">
<div
class="text-right"
v-if="!file.binary">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div class="text-right">
......
......@@ -171,10 +171,10 @@ export default {
id="ide"
class="blob-viewer-container blob-editor-container"
>
<div
class="ide-mode-tabs clearfix"
v-if="!shouldHideEditor">
<ul class="nav-links pull-left">
<div class="ide-mode-tabs clearfix">
<ul
class="nav-links pull-left"
v-if="!shouldHideEditor">
<li :class="editTabCSS">
<a
href="javascript:void(0);"
......@@ -210,9 +210,10 @@ export default {
>
</div>
<content-viewer
v-if="!shouldHideEditor && file.viewMode === 'preview'"
v-if="shouldHideEditor || file.viewMode === 'preview'"
:content="file.content || file.raw"
:path="file.path"
:path="file.rawPath"
:file-size="file.size"
:project-path="file.projectId"/>
</div>
</template>
......@@ -43,6 +43,7 @@ export default {
raw: null,
baseRaw: null,
html: data.html,
size: data.size,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
......
......@@ -40,6 +40,7 @@ export const dataStructure = () => ({
eol: '',
viewMode: 'edit',
previewMode: null,
size: 0,
});
export const decorateData = entity => {
......
<script>
import { viewerInformationForPath } from './lib/viewer_utils';
import MarkdownViewer from './viewers/markdown_viewer.vue';
import ImageViewer from './viewers/image_viewer.vue';
import DownloadViewer from './viewers/download_viewer.vue';
export default {
props: {
content: {
type: String,
required: true,
default: '',
},
path: {
type: String,
required: true,
},
fileSize: {
type: Number,
required: false,
default: 0,
},
projectPath: {
type: String,
required: false,
......@@ -20,12 +27,18 @@ export default {
},
computed: {
viewer() {
if (!this.path) return null;
const previewInfo = viewerInformationForPath(this.path);
if (!previewInfo) return DownloadViewer;
switch (previewInfo.id) {
case 'markdown':
return MarkdownViewer;
case 'image':
return ImageViewer;
default:
return null;
return DownloadViewer;
}
},
},
......@@ -36,6 +49,8 @@ export default {
<div class="preview-container">
<component
:is="viewer"
:path="path"
:file-size="fileSize"
:project-path="projectPath"
:content="content"
/>
......
const viewers = {
image: {
id: 'image',
},
markdown: {
id: 'markdown',
previewTitle: 'Preview Markdown',
......@@ -7,6 +10,12 @@ const viewers = {
const fileNameViewers = {};
const fileExtensionViewers = {
jpg: 'image',
jpeg: 'image',
gif: 'image',
png: 'image',
bmp: 'image',
ico: 'image',
md: '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>
......@@ -524,17 +524,17 @@
svg {
fill: $gl-text-color-secondary;
position: relative;
left: 5px;
top: 2px;
width: 18px;
height: 18px;
left: 1px;
top: -1px;
width: 16px;
height: 16px;
}
&.play {
svg {
width: #{$ci-action-icon-size - 8};
height: #{$ci-action-icon-size - 8};
left: 8px;
width: 16px;
height: 16px;
left: 3px;
}
}
}
......
......@@ -312,6 +312,45 @@
height: 100%;
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 {
padding: $gl-padding;
}
......
......@@ -30,6 +30,8 @@ class Commit
MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH
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)
pipeline = field == :description ? :commit_description : :single_line
......@@ -143,7 +145,8 @@ class Commit
end
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
def to_reference(from = nil, full: false)
......
- illustration = local_assigns.fetch(:illustration)
- illustration_size = local_assigns.fetch(:illustration_size)
- title = local_assigns.fetch(:title)
- content = local_assigns.fetch(:content)
- content = local_assigns.fetch(:content, nil)
- action = local_assigns.fetch(:action, nil)
.row.empty-state
......@@ -11,7 +11,8 @@
.col-xs-12
.text-content
%h4.text-center= title
%p= content
- if content
%p= content
- if action
.text-center
= 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 @@
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- if @build.started?
- if @build.has_trace?
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
.js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
......@@ -90,25 +91,9 @@
%pre.build-trace#build-trace
%code.bash.js-build-output
.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
= render 'empty_state',
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 "empty_states"
= render "sidebar", builds: @builds
.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
......@@ -11,7 +11,7 @@ module SharedBuilds
step 'project has a recent build' do
@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
step 'recent build is successful' do
......
......@@ -215,6 +215,10 @@ module Banzai
extras << "comment #{$1}"
end
extension = matches[:extension] if matches.names.include?("extension")
extras << extension if extension
extras
end
......
......@@ -23,6 +23,10 @@ module Gitlab
'Cancel'
end
def action_button_title
_('Cancel this job')
end
def self.matches?(build, user)
build.cancelable?
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
module Build
class Factory < Status::Factory
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::Failed],
[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
'Play'
end
def action_button_title
_('Trigger this manual action')
end
def action_path
play_project_job_path(subject.project, subject)
end
......
......@@ -15,6 +15,10 @@ module Gitlab
'Retry'
end
def action_button_title
_('Retry this job')
end
def action_path
retry_project_job_path(subject.project, subject)
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
'Stop'
end
def action_button_title
_('Stop this environment')
end
def action_path
play_project_job_path(subject.project, subject)
end
......
......@@ -22,6 +22,10 @@ module Gitlab
raise NotImplementedError
end
def illustration
raise NotImplementedError
end
def label
raise NotImplementedError
end
......@@ -58,6 +62,10 @@ module Gitlab
raise NotImplementedError
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
def status_tooltip
label
......
......@@ -23,7 +23,7 @@ module Gitlab
end
rescue GRPC::FailedPrecondition => e
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)
end
......
......@@ -1375,6 +1375,18 @@ module Gitlab
raise CommandError.new(e)
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)
gitaly_migrate(:branch_names_contains_sha) do |is_enabled|
if is_enabled
......@@ -1479,6 +1491,33 @@ module Gitlab
run_git!(['rev-list', '--max-count=1', oldrev, "^#{newrev}"])
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 checksum
gitaly_migrate(:calculate_checksum) do |is_enabled|
if is_enabled
......@@ -1575,33 +1614,6 @@ module Gitlab
File.exist?(path) && !clean_stuck_worktree(path)
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)
return false unless File.mtime(path) < 15.minutes.ago
......
......@@ -251,6 +251,11 @@ module Gitlab
end
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)
push_size_in_bytes = 0
......
......@@ -19,6 +19,11 @@ module Gitlab
response.exists
end
def cleanup
request = Gitaly::CleanupRequest.new(repository: @gitaly_repo)
GitalyClient.call(@storage, :repository_service, :cleanup, request)
end
def garbage_collect(create_bitmap)
request = Gitaly::GarbageCollectRequest.new(repository: @gitaly_repo, create_bitmap: create_bitmap)
GitalyClient.call(@storage, :repository_service, :garbage_collect, request)
......
......@@ -9,6 +9,7 @@ describe 'Merge request < User sees mini pipeline graph', :js do
before do
build.run
build.trace.set('hello')
sign_in(user)
visit_merge_request
end
......@@ -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') }
before do
create(:ci_build, pipeline: pipeline, legacy_artifacts_file: artifacts_file1)
create(:ci_build, pipeline: pipeline, when: 'manual')
create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file1)
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
end
it 'avoids repeated database queries' do
before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
create(:ci_build, pipeline: pipeline, legacy_artifacts_file: artifacts_file2)
create(:ci_build, pipeline: pipeline, when: 'manual')
create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file2)
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
......
require 'spec_helper'
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_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
project.add_master(user)
project.enable_ci
build.success
build.trace.set('job trace')
sign_in(user)
......@@ -21,7 +20,9 @@ describe 'User browses a job', :js do
expect(page).to have_content("Job ##{build.id}")
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(build).not_to have_trace
......@@ -36,7 +37,7 @@ describe 'User browses a job', :js do
end
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
within('.builds-container') do
......@@ -47,7 +48,7 @@ describe 'User browses a job', :js do
end
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
within('.builds-container') do
......
......@@ -113,7 +113,7 @@ feature 'Jobs' do
describe "GET /:project/jobs/:id" 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
visit project_job_path(project, job)
......@@ -136,7 +136,7 @@ feature 'Jobs' do
end
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
visit project_job_path(project, job)
......@@ -153,7 +153,7 @@ feature 'Jobs' do
end
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
visit project_job_path(project, job)
......@@ -339,7 +339,7 @@ feature 'Jobs' do
context 'job is successfull and has deployment' do
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
visit project_job_path(project, job)
......@@ -349,7 +349,7 @@ feature 'Jobs' do
end
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
visit project_job_path(project, job)
......@@ -360,7 +360,7 @@ feature 'Jobs' do
context 'job creates a new deployment' do
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
visit project_job_path(project, job)
......@@ -390,6 +390,7 @@ feature 'Jobs' do
end
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 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')
......@@ -413,6 +414,7 @@ feature 'Jobs' do
end
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 depends on upstream jobs that need to succeed in order for this job to be triggered')
end
......@@ -426,10 +428,53 @@ feature 'Jobs' do
end
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 is in pending state and is waiting to be picked by a runner')
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
describe "POST /:project/jobs/:id/cancel", :js do
......
......@@ -38,4 +38,33 @@ describe('ContentViewer', () => {
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
expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>})
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
......@@ -90,6 +90,10 @@ describe Gitlab::Ci::Status::Build::Cancelable do
describe '#action_title' do
it { expect(subject.action_title).to eq 'Cancel' }
end
describe '#action_button_title' do
it { expect(subject.action_button_title).to eq 'Cancel this job' }
end
end
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
end
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
expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
......@@ -38,6 +38,33 @@ describe Gitlab::Ci::Status::Build::Factory do
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 not allowed to fail' do
let(:build) { create(:ci_build, :failed) }
......@@ -106,7 +133,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Retryable]
.to eq [Gitlab::Ci::Status::Build::Canceled, Gitlab::Ci::Status::Build::Retryable]
end
it 'fabricates a retryable build status' do
......@@ -117,6 +144,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.text).to eq 'canceled'
expect(status.icon).to eq '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).to have_details
expect(status).to have_action
......@@ -158,7 +186,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Cancelable]
.to eq [Gitlab::Ci::Status::Build::Pending, Gitlab::Ci::Status::Build::Cancelable]
end
it 'fabricates a cancelable build status' do
......@@ -169,6 +197,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.text).to eq 'pending'
expect(status.icon).to eq '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).to have_details
expect(status).to have_action
......@@ -182,18 +211,19 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
end
it 'does not match extended statuses' do
expect(factory.extended_statuses).to be_empty
it 'matches correct extended statuses' do
expect(factory.extended_statuses).to eq [Gitlab::Ci::Status::Build::Skipped]
end
it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Skipped
it 'fabricates a skipped build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Skipped
end
it 'fabricates status with correct details' do
expect(status.text).to eq 'skipped'
expect(status.icon).to eq '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).to have_details
expect(status).not_to have_action
......@@ -210,7 +240,8 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do
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]
end
......@@ -223,6 +254,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.group).to eq 'manual'
expect(status.icon).to eq '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).to have_details
expect(status.action_path).to include 'play'
......@@ -257,7 +289,8 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do
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]
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
it { expect(subject.action_title).to eq 'Play' }
end
describe '#action_button_title' do
it { expect(subject.action_button_title).to eq 'Trigger this manual action' }
end
describe '.matches?' do
subject { described_class.matches?(build, user) }
......
......@@ -90,6 +90,10 @@ describe Gitlab::Ci::Status::Build::Retryable do
describe '#action_title' do
it { expect(subject.action_title).to eq 'Retry' }
end
describe '#action_button_title' do
it { expect(subject.action_button_title).to eq 'Retry this job' }
end
end
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
describe '#action_title' do
it { expect(subject.action_title).to eq 'Stop' }
end
describe '#action_button_title' do
it { expect(subject.action_button_title).to eq 'Stop this environment' }
end
end
describe '.matches?' do
......
......@@ -2306,6 +2306,39 @@ describe Gitlab::Git::Repository, seed_helper: true do
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
subject do
repository.delete_remote_branches('downstream-remote', ['master'])
......
......@@ -1128,6 +1128,20 @@ describe Gitlab::GitAccess do
end
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
end
end
end
describe 'build authentication abilities' do
......
......@@ -17,6 +17,16 @@ describe Gitlab::GitalyClient::RepositoryService do
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
it 'sends a garbage_collect message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
......
......@@ -77,6 +77,14 @@ describe MergeRequests::Conflicts::ListService do
expect(service.can_be_resolved_in_ui?).to be_falsey
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
it 'returns a falsey value when the MR has a missing ref after a force push' do
merge_request = create_merge_request('conflict-resolvable')
......@@ -85,6 +93,14 @@ describe MergeRequests::Conflicts::ListService do
expect(service.can_be_resolved_in_ui?).to be_falsey
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
......@@ -21,7 +21,7 @@ describe 'projects/jobs/show' do
describe 'environment info in job view' do
context 'job with latest deployment' do
let(:build) do
create(:ci_build, :success, environment: 'staging')
create(:ci_build, :success, :trace_artifact, environment: 'staging')
end
before do
......@@ -40,11 +40,11 @@ describe 'projects/jobs/show' do
context 'job with outdated deployment' do
let(:build) do
create(:ci_build, :success, environment: 'staging', pipeline: pipeline)
create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline)
end
let(:second_build) do
create(:ci_build, :success, environment: 'staging', pipeline: pipeline)
create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline)
end
let(:environment) do
......@@ -70,7 +70,7 @@ describe 'projects/jobs/show' do
context 'job failed to deploy' do
let(:build) do
create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
create(:ci_build, :failed, :trace_artifact, environment: 'staging', pipeline: pipeline)
end
let!(:environment) do
......@@ -88,7 +88,7 @@ describe 'projects/jobs/show' do
context 'job will deploy' do
let(:build) do
create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
create(:ci_build, :running, :trace_live, environment: 'staging', pipeline: pipeline)
end
context 'when environment exists' do
......@@ -136,7 +136,7 @@ describe 'projects/jobs/show' do
context 'job that failed to deploy and environment has not been created' do
let(:build) do
create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
create(:ci_build, :failed, :trace_artifact, environment: 'staging', pipeline: pipeline)
end
let!(:environment) do
......@@ -154,7 +154,7 @@ describe 'projects/jobs/show' do
context 'job that will deploy and environment has not been created' do
let(:build) do
create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
create(:ci_build, :running, :trace_live, environment: 'staging', pipeline: pipeline)
end
let!(:environment) do
......@@ -174,8 +174,9 @@ describe 'projects/jobs/show' do
end
context 'when job is running' do
let(:build) { create(:ci_build, :trace_live, :running, pipeline: pipeline) }
before do
build.run!
render
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