summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/api.js7
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue2
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue4
-rw-r--r--app/assets/javascripts/diffs/store/utils.js11
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue11
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue6
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue32
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue21
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue35
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue3
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue6
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue17
-rw-r--r--app/assets/javascripts/pages/projects/releases/index/index.js3
-rw-r--r--app/assets/javascripts/releases/components/app.vue82
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue89
-rw-r--r--app/assets/javascripts/releases/index.js24
-rw-r--r--app/assets/javascripts/releases/store/actions.js37
-rw-r--r--app/assets/javascripts/releases/store/index.js14
-rw-r--r--app/assets/javascripts/releases/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/releases/store/mutations.js37
-rw-r--r--app/assets/javascripts/releases/store/state.js5
-rw-r--r--app/assets/stylesheets/pages/builds.scss5
-rw-r--r--app/controllers/concerns/lfs_request.rb6
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb2
-rw-r--r--app/models/broadcast_message.rb37
-rw-r--r--app/models/concerns/avatarable.rb13
-rw-r--r--app/models/environment.rb5
-rw-r--r--app/models/pool_repository.rb17
-rw-r--r--app/models/project.rb4
-rw-r--r--app/serializers/environment_entity.rb16
-rw-r--r--app/services/deploy_keys/create_service.rb2
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/views/projects/releases/index.html.haml4
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/object_pool/destroy_worker.rb16
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb6
-rw-r--r--changelogs/unreleased/23367-clarify-docs-allow-failure.yml5
-rw-r--r--changelogs/unreleased/41766-vuex-store.yml5
-rw-r--r--changelogs/unreleased/51606-expanding-a-diff-while-having-an-open-comment-form-will-always-scroll-down-to-the-comment.yml5
-rw-r--r--changelogs/unreleased/54814-sidebar-styling-updates.yml5
-rw-r--r--changelogs/unreleased/54953-fix-commit_email_hostname-accessor-in-fake_application_settings.yml5
-rw-r--r--changelogs/unreleased/55103-hide-group-cluster-features.yml5
-rw-r--r--changelogs/unreleased/depracated-migration-inheritance.yml5
-rw-r--r--changelogs/unreleased/mk-avoid-read-only-error.yml5
-rw-r--r--changelogs/unreleased/sh-cache-avatar-paths.yml5
-rw-r--r--changelogs/unreleased/winh-upgrade-gitlab-ui.yml5
-rw-r--r--danger/documentation/Dangerfile2
-rw-r--r--db/migrate/20181006004100_import_common_metrics_nginx_vts.rb2
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/api/jobs.md140
-rw-r--r--doc/ci/merge_request_pipelines/index.md31
-rw-r--r--doc/ci/yaml/README.md19
-rw-r--r--doc/ssh/README.md21
-rw-r--r--doc/user/abuse_reports.md53
-rw-r--r--doc/user/admin_area/abuse_reports.md31
-rw-r--r--doc/user/admin_area/img/abuse_report_blocked_user.pngbin0 -> 13821 bytes
-rw-r--r--doc/user/admin_area/img/abuse_reports_page.pngbin0 -> 215813 bytes
-rw-r--r--doc/user/index.md1
-rw-r--r--doc/user/profile/account/delete_account.md3
-rw-r--r--doc/user/project/img/releases.pngbin0 -> 43612 bytes
-rw-r--r--doc/user/project/releases.md12
-rw-r--r--lib/gitlab/fake_application_settings.rb4
-rw-r--r--lib/gitlab/git/object_pool.rb2
-rw-r--r--lib/gitlab/gitaly_client/object_pool_service.rb5
-rw-r--r--lib/gitlab/json_cache.rb87
-rw-r--r--lib/tasks/gitlab/storage.rake10
-rw-r--r--locale/gitlab.pot30
-rw-r--r--package.json2
-rw-r--r--spec/controllers/projects/deploy_keys_controller_spec.rb36
-rw-r--r--spec/factories/pool_repositories.rb4
-rw-r--r--spec/features/projects/jobs_spec.rb8
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js20
-rw-r--r--spec/javascripts/environments/environment_terminal_button_spec.js48
-rw-r--r--spec/javascripts/jobs/components/artifacts_block_spec.js13
-rw-r--r--spec/javascripts/jobs/components/sidebar_spec.js8
-rw-r--r--spec/javascripts/releases/components/app_spec.js79
-rw-r--r--spec/javascripts/releases/components/release_block_spec.js34
-rw-r--r--spec/javascripts/releases/mock_data.js128
-rw-r--r--spec/javascripts/releases/store/actions_spec.js98
-rw-r--r--spec/javascripts/releases/store/helpers.js6
-rw-r--r--spec/javascripts/releases/store/mutations_spec.js47
-rw-r--r--spec/lib/gitlab/json_cache_spec.rb401
-rw-r--r--spec/lib/gitlab/prometheus/query_variables_spec.rb4
-rw-r--r--spec/models/broadcast_message_spec.rb23
-rw-r--r--spec/models/concerns/avatarable_spec.rb37
-rw-r--r--spec/models/pool_repository_spec.rb21
-rw-r--r--spec/serializers/environment_entity_spec.rb30
-rw-r--r--spec/tasks/gitlab/storage_rake_spec.rb14
-rw-r--r--spec/workers/object_pool/destroy_worker_spec.rb31
-rw-r--r--spec/workers/stuck_merge_jobs_worker_spec.rb3
-rw-r--r--yarn.lock17
93 files changed, 1839 insertions, 368 deletions
diff --git a/Gemfile b/Gemfile
index 6ada5eede27..c41f14e09d2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -419,7 +419,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 1.3.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 1.5.0', require: 'gitaly'
gem 'grpc', '~> 1.15.0'
gem 'google-protobuf', '~> 3.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 6d21089c82c..23a2bbeffdd 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -274,7 +274,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.3.0)
+ gitaly-proto (1.5.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
@@ -1008,7 +1008,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.3.0)
+ gitaly-proto (~> 1.5.0)
github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
gitlab-markup (~> 1.6.5)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 7607c4b3b79..a1310d18c26 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -29,6 +29,7 @@ const Api = {
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
+ releasesPath: '/api/:version/project/:id/releases',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -307,6 +308,12 @@ const Api = {
});
},
+ releases(id) {
+ const url = Api.buildUrl(this.releasesPath).replace(':id', encodeURIComponent(id));
+
+ return axios.get(url);
+ },
+
buildUrl(url) {
let urlRoot = '';
if (gon.relative_url_root != null) {
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index 9310e2b7ca9..e781397214d 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -49,7 +49,7 @@ export default {
:is-bottom="index + 1 === diffLinesLength"
/>
<inline-diff-comment-row
- :key="`icr-${index}`"
+ :key="`icr-${line.line_code || index}`"
:diff-file-hash="diffFile.file_hash"
:line="line"
:help-page-path="helpPagePath"
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index e6bc0daebb3..1bf693380db 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -43,14 +43,14 @@ export default {
<tbody>
<template v-for="(line, index) in diffLines">
<parallel-diff-table-row
- :key="index"
+ :key="line.line_code"
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
/>
<parallel-diff-comment-row
- :key="`dcr-${index}`"
+ :key="`dcr-${line.line_code || index}`"
:line="line"
:diff-file-hash="diffFile.file_hash"
:line-index="index"
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index cbaa0e26395..2fe20551642 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -196,6 +196,15 @@ export function trimFirstCharOfLineContent(line = {}) {
return parsedLine;
}
+function getLineCode({ left, right }, index) {
+ if (left && left.line_code) {
+ return left.line_code;
+ } else if (right && right.line_code) {
+ return right.line_code;
+ }
+ return index;
+}
+
// This prepares and optimizes the incoming diff data from the server
// by setting up incremental rendering and removing unneeded data
export function prepareDiffData(diffData) {
@@ -208,6 +217,8 @@ export function prepareDiffData(diffData) {
const linesLength = file.parallel_diff_lines.length;
for (let u = 0; u < linesLength; u += 1) {
const line = file.parallel_diff_lines[u];
+
+ line.line_code = getLineCode(line, u);
if (line.left) {
line.left = trimFirstCharOfLineContent(line.left);
line.left.hasForm = false;
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index cd2f46fd07a..f44806d82a6 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -14,6 +14,7 @@ import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { CLUSTER_TYPE } from '~/clusters/constants';
/**
* Environment Item Component
@@ -85,6 +86,15 @@ export default {
},
/**
+ * Hide group cluster features which are not currently implemented.
+ *
+ * @returns {Boolean}
+ */
+ disableGroupClusterFeatures() {
+ return this.model && this.model.cluster_type === CLUSTER_TYPE.GROUP;
+ },
+
+ /**
* Returns whether the environment can be stopped.
*
* @returns {Boolean}
@@ -547,6 +557,7 @@ export default {
<terminal-button-component
v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"
+ :disabled="disableGroupClusterFeatures"
/>
<rollback-component
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 83727caad16..6d74d136a94 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -19,6 +19,11 @@ export default {
required: false,
default: '',
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
title() {
@@ -33,6 +38,7 @@ export default {
:title="title"
:aria-label="title"
:href="terminalPath"
+ :class="{ disabled: disabled }"
class="btn terminal-button d-none d-sm-none d-md-block"
>
<icon name="terminal" />
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 309b7427b9e..0bce860df91 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -28,27 +28,29 @@ export default {
</script>
<template>
<div class="block">
- <div class="title">{{ s__('Job|Job artifacts') }}</div>
+ <div class="title font-weight-bold">{{ s__('Job|Job artifacts') }}</div>
- <p v-if="isExpired" class="js-artifacts-removed build-detail-row">
- {{ s__('Job|The artifacts were removed') }}
+ <p
+ v-if="isExpired || willExpire"
+ :class="{
+ 'js-artifacts-removed': isExpired,
+ 'js-artifacts-will-be-removed': willExpire,
+ }"
+ class="build-detail-row"
+ >
+ <span v-if="isExpired">{{ s__('Job|The artifacts were removed') }}</span>
+ <span v-if="willExpire">{{ s__('Job|The artifacts will be removed') }}</span>
+ <timeago-tooltip v-if="artifact.expire_at" :time="artifact.expire_at" />
</p>
- <p v-else-if="willExpire" class="js-artifacts-will-be-removed build-detail-row">
- {{ s__('Job|The artifacts will be removed in') }}
- </p>
-
- <timeago-tooltip v-if="artifact.expire_at" :time="artifact.expire_at" />
-
- <div class="btn-group d-flex" role="group">
+ <div class="btn-group d-flex prepend-top-10" role="group">
<gl-link
v-if="artifact.keep_path"
:href="artifact.keep_path"
class="js-keep-artifacts btn btn-sm btn-default"
data-method="post"
+ >{{ s__('Job|Keep') }}</gl-link
>
- {{ s__('Job|Keep') }}
- </gl-link>
<gl-link
v-if="artifact.download_path"
@@ -56,17 +58,15 @@ export default {
class="js-download-artifacts btn btn-sm btn-default"
download
rel="nofollow"
+ >{{ s__('Job|Download') }}</gl-link
>
- {{ s__('Job|Download') }}
- </gl-link>
<gl-link
v-if="artifact.browse_path"
:href="artifact.browse_path"
class="js-browse-artifacts btn btn-sm btn-default"
+ >{{ s__('Job|Browse') }}</gl-link
>
- {{ s__('Job|Browse') }}
- </gl-link>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
index 3b9c61bd48c..e0f55518eef 100644
--- a/app/assets/javascripts/jobs/components/commit_block.vue
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -31,12 +31,12 @@ export default {
block: !isLastBlock,
}"
>
- <p>
- {{ __('Commit') }}
+ <p class="append-bottom-5">
+ <span class="font-weight-bold">{{ __('Commit') }}</span>
- <gl-link :href="commit.commit_path" class="js-commit-sha commit-sha link-commit">{{
- commit.short_id
- }}</gl-link>
+ <gl-link :href="commit.commit_path" class="js-commit-sha commit-sha link-commit">
+ {{ commit.short_id }}
+ </gl-link>
<clipboard-button
:text="commit.short_id"
@@ -44,11 +44,14 @@ export default {
css-class="btn btn-clipboard btn-transparent"
/>
- <gl-link v-if="mergeRequest" :href="mergeRequest.path" class="js-link-commit link-commit"
- >!{{ mergeRequest.iid }}</gl-link
- >
+ <span v-if="mergeRequest">
+ {{ __('in') }}
+ <gl-link :href="mergeRequest.path" class="js-link-commit link-commit"
+ >!{{ mergeRequest.iid }}</gl-link
+ >
+ </span>
</p>
- <p class="build-light-text append-bottom-0">{{ commit.title }}</p>
+ <p class="append-bottom-0">{{ commit.title }}</p>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 934ecd0e3ec..ad3e7dabc79 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -110,22 +110,20 @@ export default {
<aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
<div class="sidebar-container">
<div class="blocks-container">
- <div class="block">
- <strong class="inline prepend-top-8"> {{ job.name }} </strong>
+ <div class="block d-flex align-items-center">
+ <h4 class="flex-grow-1 prepend-top-8 m-0">{{ job.name }}</h4>
<gl-link
v-if="job.retry_path"
:class="retryButtonClass"
:href="job.retry_path"
data-method="post"
rel="nofollow"
+ >{{ __('Retry') }}</gl-link
>
- {{ __('Retry') }}
- </gl-link>
<gl-link
v-if="job.terminal_path"
:href="job.terminal_path"
- class="js-terminal-link pull-right btn btn-primary
- btn-inverted visible-md-block visible-lg-block"
+ class="js-terminal-link pull-right btn btn-primary btn-inverted visible-md-block visible-lg-block"
target="_blank"
>
{{ __('Debug') }} <icon name="external-link" />
@@ -133,8 +131,7 @@ export default {
<gl-button
:aria-label="__('Toggle Sidebar')"
type="button"
- class="btn btn-blank gutter-toggle
- float-right d-block d-md-none js-sidebar-build-toggle"
+ class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
@click="toggleSidebar"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i>
@@ -145,25 +142,18 @@ export default {
v-if="job.new_issue_path"
:href="job.new_issue_path"
class="js-new-issue btn btn-success btn-inverted"
+ >{{ __('New issue') }}</gl-link
>
- {{ __('New issue') }}
- </gl-link>
<gl-link
v-if="job.retry_path"
:href="job.retry_path"
class="js-retry-job btn btn-inverted-secondary"
data-method="post"
rel="nofollow"
+ >{{ __('Retry') }}</gl-link
>
- {{ __('Retry') }}
- </gl-link>
</div>
<div :class="{ block: renderBlock }">
- <p v-if="job.merge_request" class="build-detail-row js-job-mr">
- <span class="build-light-text"> {{ __('Merge Request:') }} </span>
- <gl-link :href="job.merge_request.path"> !{{ job.merge_request.iid }} </gl-link>
- </p>
-
<detail-row
v-if="job.duration"
:value="duration"
@@ -198,10 +188,10 @@ export default {
title="Coverage"
/>
<p v-if="job.tags.length" class="build-detail-row js-job-tags">
- <span class="build-light-text"> {{ __('Tags:') }} </span>
- <span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary">
- {{ tag }}
- </span>
+ <span class="font-weight-bold">{{ __('Tags:') }}</span>
+ <span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary mr-1">{{
+ tag
+ }}</span>
</p>
<div v-if="job.cancel_path" class="btn-group prepend-top-5" role="group">
@@ -210,9 +200,8 @@ export default {
class="js-cancel-job btn btn-sm btn-default"
data-method="post"
rel="nofollow"
+ >{{ __('Cancel') }}</gl-link
>
- {{ __('Cancel') }}
- </gl-link>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index 77be295e802..b826007ec2c 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -34,8 +34,7 @@ export default {
</script>
<template>
<p class="build-detail-row">
- <span v-if="hasTitle" class="build-light-text"> {{ title }}: </span> {{ value }}
-
+ <span v-if="hasTitle" class="font-weight-bold">{{ title }}:</span> {{ value }}
<span v-if="hasHelpURL" class="help-button float-right">
<gl-link :href="helpUrl" target="_blank" rel="noopener noreferrer nofollow">
<i class="fa fa-question-circle" aria-hidden="true"></i>
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index 90482500bbf..7f79e92067f 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -38,11 +38,11 @@ export default {
<div class="block-last dropdown">
<ci-icon :status="pipeline.details.status" class="vertical-align-middle" />
- {{ __('Pipeline') }}
- <a :href="pipeline.path" class="js-pipeline-path link-commit"> #{{ pipeline.id }} </a>
+ <span class="font-weight-bold">{{ __('Pipeline') }}</span>
+ <a :href="pipeline.path" class="js-pipeline-path link-commit">#{{ pipeline.id }}</a>
<template v-if="hasRef">
{{ __('from') }}
- <a :href="pipeline.ref.path" class="link-commit ref-name"> {{ pipeline.ref.name }} </a>
+ <a :href="pipeline.ref.path" class="link-commit ref-name">{{ pipeline.ref.name }}</a>
</template>
<button
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index 3cd3b743108..997737b3e23 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -43,23 +43,24 @@ export default {
<template>
<div class="build-widget block">
- <h4 class="title">{{ __('Trigger') }}</h4>
-
<p
v-if="trigger.short_token"
class="js-short-token"
- :class="{ 'append-bottom-0': !hasVariables }"
+ :class="{ 'append-bottom-5': hasVariables, 'append-bottom-0': !hasVariables }"
>
- <span class="build-light-text"> {{ __('Token') }} </span> {{ trigger.short_token }}
+ <span class="font-weight-bold">{{ __('Trigger token:') }}</span> {{ trigger.short_token }}
</p>
<template v-if="hasVariables">
<p class="trigger-variables-btn-container">
- <span class="build-light-text"> {{ __('Variables:') }} </span>
+ <span class="font-weight-bold">{{ __('Trigger variables:') }}</span>
- <gl-button v-if="hasValues" class="group js-reveal-variables" @click="toggleValues">
- {{ getToggleButtonText }}
- </gl-button>
+ <gl-button
+ v-if="hasValues"
+ class="btn-sm group js-reveal-variables trigger-variables-btn"
+ @click="toggleValues"
+ >{{ getToggleButtonText }}</gl-button
+ >
</p>
<table class="js-build-variables trigger-build-variables">
diff --git a/app/assets/javascripts/pages/projects/releases/index/index.js b/app/assets/javascripts/pages/projects/releases/index/index.js
new file mode 100644
index 00000000000..c183fbb9610
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/releases/index/index.js
@@ -0,0 +1,3 @@
+import initReleases from '~/releases';
+
+document.addEventListener('DOMContentLoaded', initReleases);
diff --git a/app/assets/javascripts/releases/components/app.vue b/app/assets/javascripts/releases/components/app.vue
new file mode 100644
index 00000000000..0ad5ee2915c
--- /dev/null
+++ b/app/assets/javascripts/releases/components/app.vue
@@ -0,0 +1,82 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import ReleaseBlock from './release_block.vue';
+
+export default {
+ name: 'ReleasesApp',
+ components: {
+ GlLoadingIcon,
+ GlEmptyState,
+ ReleaseBlock,
+ },
+ props: {
+ projectId: {
+ type: String,
+ required: true,
+ },
+ documentationLink: {
+ type: String,
+ required: true,
+ },
+ illustrationPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['isLoading', 'releases', 'hasError']),
+ shouldRenderEmptyState() {
+ return !this.releases.length && !this.hasError && !this.isLoading;
+ },
+ shouldRenderSuccessState() {
+ return this.releases.length && !this.isLoading && !this.hasError;
+ },
+ },
+ created() {
+ this.fetchReleases(this.projectId);
+ },
+ methods: {
+ ...mapActions(['fetchReleases']),
+ },
+};
+</script>
+<template>
+ <div class="prepend-top-default">
+ <gl-loading-icon v-if="isLoading" :size="2" class="js-loading prepend-top-20" />
+
+ <gl-empty-state
+ v-else-if="shouldRenderEmptyState"
+ class="js-empty-state"
+ :title="__('Getting started with releases')"
+ :svg-path="illustrationPath"
+ :description="
+ __(
+ 'Releases mark specific points in a project\'s development history, communicate information about the type of change, and deliver on prepared, often compiled, versions of the software to be reused elsewhere. Currently, releases can only be created through the API.',
+ )
+ "
+ :primary-button-link="documentationLink"
+ :primary-button-text="__('Open Documentation')"
+ />
+
+ <div v-else-if="shouldRenderSuccessState" class="js-success-state">
+ <release-block
+ v-for="(release, index) in releases"
+ :key="release.tag_name"
+ :release="release"
+ :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
+ />
+ </div>
+ </div>
+</template>
+<style>
+.linked-card::after {
+ width: 1px;
+ content: ' ';
+ border: 1px solid #e5e5e5;
+ height: 17px;
+ top: 100%;
+ position: absolute;
+ left: 32px;
+}
+</style>
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index bd65a225d8f..9c2aade51fc 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -17,66 +17,36 @@ export default {
},
mixins: [timeagoMixin],
props: {
- name: {
- type: String,
- required: true,
- },
- tag: {
- type: String,
- required: true,
- },
- commit: {
- type: Object,
- required: true,
- },
- description: {
- type: String,
- required: false,
- default: '',
- },
- author: {
+ release: {
type: Object,
required: true,
- },
- createdAt: {
- type: String,
- required: false,
- default: '',
- },
- assetsCount: {
- type: Number,
- required: false,
- default: 0,
- },
- sources: {
- type: Array,
- required: false,
- default: () => [],
- },
- links: {
- type: Array,
- required: false,
- default: () => [],
+ default: () => ({}),
},
},
computed: {
releasedTimeAgo() {
return sprintf('released %{time}', {
- time: this.timeFormated(this.createdAt),
+ time: this.timeFormated(this.release.created_at),
});
},
userImageAltDescription() {
- return this.author && this.author.username
- ? sprintf("%{username}'s avatar", { username: this.author.username })
+ return this.commit.author && this.commit.author.username
+ ? sprintf("%{username}'s avatar", { username: this.commit.author.username })
: null;
},
+ commit() {
+ return this.release.commit || {};
+ },
+ assets() {
+ return this.release.assets || {};
+ },
},
};
</script>
<template>
<div class="card">
<div class="card-body">
- <h2 class="card-title mt-0">{{ name }}</h2>
+ <h2 class="card-title mt-0">{{ release.name }}</h2>
<div class="card-subtitle d-flex flex-wrap text-secondary">
<div class="append-right-8">
@@ -86,40 +56,47 @@ export default {
<div class="append-right-8">
<icon name="tag" class="align-middle" />
- <span v-gl-tooltip.bottom :title="__('Tag')">{{ tag }}</span>
+ <span v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span>
</div>
<div class="append-right-4">
&bull;
- <span v-gl-tooltip.bottom :title="tooltipTitle(createdAt)">{{ releasedTimeAgo }}</span>
+ <span v-gl-tooltip.bottom :title="tooltipTitle(release.created_at)">{{
+ releasedTimeAgo
+ }}</span>
</div>
- <div class="d-flex">
+ <div v-if="commit.author" class="d-flex">
by
<user-avatar-link
class="prepend-left-4"
- :link-href="author.path"
- :img-src="author.avatar_url"
+ :link-href="commit.author.path"
+ :img-src="commit.author.avatar_url"
:img-alt="userImageAltDescription"
- :tooltip-text="author.username"
+ :tooltip-text="commit.author.username"
/>
</div>
</div>
- <div class="card-text prepend-top-default">
+ <div
+ v-if="assets.links.length || assets.sources.length"
+ Sclass="card-text prepend-top-default"
+ >
<b>
- {{ __('Assets') }} <span class="js-assets-count badge badge-pill">{{ assetsCount }}</span>
+ {{ __('Assets') }}
+ <span class="js-assets-count badge badge-pill">{{ assets.count }}</span>
</b>
- <ul class="pl-0 mb-0 prepend-top-8 list-unstyled js-assets-list">
- <li v-for="link in links" :key="link.name" class="append-bottom-8">
+ <ul v-if="assets.links.length" class="pl-0 mb-0 prepend-top-8 list-unstyled js-assets-list">
+ <li v-for="link in assets.links" :key="link.name" class="append-bottom-8">
<gl-link v-gl-tooltip.bottom :title="__('Download asset')" :href="link.url">
- <icon name="package" class="align-middle append-right-4" /> {{ link.name }}
+ <icon name="package" class="align-middle append-right-4 align-text-bottom" />
+ {{ link.name }}
</gl-link>
</li>
</ul>
- <div class="dropdown">
+ <div v-if="assets.sources.length" class="dropdown">
<button
type="button"
class="btn btn-link"
@@ -132,14 +109,14 @@ export default {
</button>
<div class="js-sources-dropdown dropdown-menu">
- <li v-for="asset in sources" :key="asset.url">
+ <li v-for="asset in assets.sources" :key="asset.url">
<gl-link :href="asset.url">{{ __('Download') }} {{ asset.format }}</gl-link>
</li>
</div>
</div>
</div>
- <div class="card-text prepend-top-default"><div v-html="description"></div></div>
+ <div class="card-text prepend-top-default"><div v-html="release.description_html"></div></div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/releases/index.js b/app/assets/javascripts/releases/index.js
new file mode 100644
index 00000000000..6fa7298ac5a
--- /dev/null
+++ b/app/assets/javascripts/releases/index.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import App from './components/app.vue';
+import createStore from './store';
+
+export default () => {
+ const element = document.getElementById('js-releases-page');
+
+ return new Vue({
+ el: element,
+ store: createStore(),
+ components: {
+ App,
+ },
+ render(createElement) {
+ return createElement('app', {
+ props: {
+ endpoint: element.dataset.endpoint,
+ documentationLink: element.dataset.documentationPath,
+ illustrationPath: element.dataset.illustrationPath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/releases/store/actions.js b/app/assets/javascripts/releases/store/actions.js
new file mode 100644
index 00000000000..baa2251403e
--- /dev/null
+++ b/app/assets/javascripts/releases/store/actions.js
@@ -0,0 +1,37 @@
+import * as types from './mutation_types';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import api from '~/api';
+
+/**
+ * Commits a mutation to update the state while the main endpoint is being requested.
+ */
+export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES);
+
+/**
+ * Fetches the main endpoint.
+ * Will dispatch requestNamespace action before starting the request.
+ * Will dispatch receiveNamespaceSuccess if the request is successfull
+ * Will dispatch receiveNamesapceError if the request returns an error
+ *
+ * @param {String} projectId
+ */
+export const fetchReleases = ({ dispatch }, projectId) => {
+ dispatch('requestReleases');
+
+ api
+ .releases(projectId)
+ .then(({ data }) => dispatch('receiveReleasesSuccess', data))
+ .catch(() => dispatch('receiveReleasesError'));
+};
+
+export const receiveReleasesSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_RELEASES_SUCCESS, data);
+
+export const receiveReleasesError = ({ commit }) => {
+ commit(types.RECEIVE_RELEASES_ERROR);
+ createFlash(__('An error occured while fetching the releases. Please try again.'));
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/releases/store/index.js b/app/assets/javascripts/releases/store/index.js
new file mode 100644
index 00000000000..968b94f0e0d
--- /dev/null
+++ b/app/assets/javascripts/releases/store/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ state: state(),
+ });
diff --git a/app/assets/javascripts/releases/store/mutation_types.js b/app/assets/javascripts/releases/store/mutation_types.js
new file mode 100644
index 00000000000..a74bf15c515
--- /dev/null
+++ b/app/assets/javascripts/releases/store/mutation_types.js
@@ -0,0 +1,3 @@
+export const REQUEST_RELEASES = 'REQUEST_RELEASES';
+export const RECEIVE_RELEASES_SUCCESS = 'RECEIVE_RELEASES_SUCCESS';
+export const RECEIVE_RELEASES_ERROR = 'RECEIVE_RELEASES_ERROR';
diff --git a/app/assets/javascripts/releases/store/mutations.js b/app/assets/javascripts/releases/store/mutations.js
new file mode 100644
index 00000000000..b97dc6cb0ab
--- /dev/null
+++ b/app/assets/javascripts/releases/store/mutations.js
@@ -0,0 +1,37 @@
+import * as types from './mutation_types';
+
+export default {
+ /**
+ * Sets isLoading to true while the request is being made.
+ * @param {Object} state
+ */
+ [types.REQUEST_RELEASES](state) {
+ state.isLoading = true;
+ },
+
+ /**
+ * Sets isLoading to false.
+ * Sets hasError to false.
+ * Sets the received data
+ * @param {Object} state
+ * @param {Object} data
+ */
+ [types.RECEIVE_RELEASES_SUCCESS](state, data) {
+ state.hasError = false;
+ state.isLoading = false;
+ state.releases = data;
+ },
+
+ /**
+ * Sets isLoading to false.
+ * Sets hasError to true.
+ * Resets the data
+ * @param {Object} state
+ * @param {Object} data
+ */
+ [types.RECEIVE_RELEASES_ERROR](state) {
+ state.isLoading = false;
+ state.releases = [];
+ state.hasError = true;
+ },
+};
diff --git a/app/assets/javascripts/releases/store/state.js b/app/assets/javascripts/releases/store/state.js
new file mode 100644
index 00000000000..bf25e651c99
--- /dev/null
+++ b/app/assets/javascripts/releases/store/state.js
@@ -0,0 +1,5 @@
+export default () => ({
+ isLoading: false,
+ hasError: false,
+ releases: [],
+});
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 57918eafd6f..c7dde2f0f2a 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -232,6 +232,11 @@
@extend .d-flex;
justify-content: space-between;
align-items: center;
+
+ .trigger-variables-btn {
+ margin-top: -5px;
+ margin-bottom: -5px;
+ }
}
.trigger-build-variables {
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 9576eb14fdd..5572c3cee2d 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -94,6 +94,7 @@ module LfsRequest
def lfs_upload_access?
return false unless project.lfs_enabled?
return false unless has_authentication_ability?(:push_code)
+ return false if limit_exceeded?
lfs_deploy_token? || can?(user, :push_code, project)
end
@@ -121,4 +122,9 @@ module LfsRequest
def has_authentication_ability?(capability)
(authentication_abilities || []).include?(capability)
end
+
+ # Overriden in EE
+ def limit_exceeded?
+ false
+ end
end
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 0a593bd35b6..6824a07dc76 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -24,7 +24,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def create
- @key = DeployKeys::CreateService.new(current_user, create_params).execute
+ @key = DeployKeys::CreateService.new(current_user, create_params).execute(project: @project)
unless @key.valid?
flash[:alert] = @key.errors.full_messages.join(', ').html_safe
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 277f7c2717c..2d237383e60 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -22,49 +22,30 @@ class BroadcastMessage < ActiveRecord::Base
after_commit :flush_redis_cache
def self.current
- raw_messages = Rails.cache.fetch(CACHE_KEY, expires_in: cache_expires_in) do
+ messages = cache.fetch(CACHE_KEY, as: BroadcastMessage, expires_in: cache_expires_in) do
remove_legacy_cache_key
- current_and_future_messages.to_json
+ current_and_future_messages
end
- messages = decode_messages(raw_messages)
-
return [] unless messages&.present?
now_or_future = messages.select(&:now_or_future?)
# If there are cached entries but none are to be displayed we'll purge the
# cache so we don't keep running this code all the time.
- Rails.cache.delete(CACHE_KEY) if now_or_future.empty?
+ cache.expire(CACHE_KEY) if now_or_future.empty?
now_or_future.select(&:now?)
end
- def self.decode_messages(raw_messages)
- return unless raw_messages&.present?
-
- message_list = ActiveSupport::JSON.decode(raw_messages)
-
- return unless message_list.is_a?(Array)
-
- valid_attr = BroadcastMessage.attribute_names
-
- message_list.map do |raw|
- BroadcastMessage.new(raw) if valid_cache_entry?(raw, valid_attr)
- end.compact
- rescue ActiveSupport::JSON.parse_error
- end
-
- def self.valid_cache_entry?(raw, valid_attr)
- return false unless raw.is_a?(Hash)
-
- (raw.keys - valid_attr).empty?
- end
-
def self.current_and_future_messages
where('ends_at > :now', now: Time.zone.now).order_id_asc
end
+ def self.cache
+ Gitlab::JsonCache.new(cache_key_with_version: false)
+ end
+
def self.cache_expires_in
nil
end
@@ -74,7 +55,7 @@ class BroadcastMessage < ActiveRecord::Base
# environment a one-shot migration would not work because the cache
# would be repopulated by a node that has not been upgraded.
def self.remove_legacy_cache_key
- Rails.cache.delete(LEGACY_CACHE_KEY)
+ cache.expire(LEGACY_CACHE_KEY)
end
def active?
@@ -102,7 +83,7 @@ class BroadcastMessage < ActiveRecord::Base
end
def flush_redis_cache
- Rails.cache.delete(CACHE_KEY)
+ self.class.cache.expire(CACHE_KEY)
self.class.remove_legacy_cache_key
end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index b42236c1fa2..4687ec7d166 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -43,7 +43,18 @@ module Avatarable
end
def avatar_path(only_path: true, size: nil)
- return unless self[:avatar].present?
+ unless self.try(:id)
+ return uncached_avatar_path(only_path: only_path, size: size)
+ end
+
+ # Cache this avatar path only within the request because avatars in
+ # object storage may be generated with time-limited, signed URLs.
+ key = "#{self.class.name}:#{self.id}:#{only_path}:#{size}"
+ Gitlab::SafeRequestStore[key] ||= uncached_avatar_path(only_path: only_path, size: size)
+ end
+
+ def uncached_avatar_path(only_path: true, size: nil)
+ return unless self.try(:avatar).present?
asset_host = ActionController::Base.asset_host
use_asset_host = asset_host.present?
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 934828946b9..cdfe3b7c023 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class Environment < ActiveRecord::Base
+ include Gitlab::Utils::StrongMemoize
# Used to generate random suffixes for the slug
LETTERS = 'a'..'z'
NUMBERS = '0'..'9'
@@ -231,7 +232,9 @@ class Environment < ActiveRecord::Base
end
def deployment_platform
- project.deployment_platform(environment: self.name)
+ strong_memoize(:deployment_platform) do
+ project.deployment_platform(environment: self.name)
+ end
end
private
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index 47da0209c2f..ad6a008dee8 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -18,6 +18,7 @@ class PoolRepository < ActiveRecord::Base
state :scheduled
state :ready
state :failed
+ state :obsolete
event :schedule do
transition none: :scheduled
@@ -31,6 +32,10 @@ class PoolRepository < ActiveRecord::Base
transition all => :failed
end
+ event :mark_obsolete do
+ transition all => :obsolete
+ end
+
state all - [:ready] do
def joinable?
false
@@ -54,6 +59,12 @@ class PoolRepository < ActiveRecord::Base
::ObjectPool::ScheduleJoinWorker.perform_async(pool.id)
end
end
+
+ after_transition any => :obsolete do |pool, _|
+ pool.run_after_commit do
+ ::ObjectPool::DestroyWorker.perform_async(pool.id)
+ end
+ end
end
def create_object_pool
@@ -71,10 +82,10 @@ class PoolRepository < ActiveRecord::Base
end
# This RPC can cause data loss, as not all objects are present the local repository
- # No execution path yet, will be added through:
- # https://gitlab.com/gitlab-org/gitaly/issues/1415
- def delete_repository_alternate(repository)
+ def unlink_repository(repository)
object_pool.unlink_repository(repository.raw)
+
+ mark_obsolete unless member_projects.where.not(id: repository.project.id).exists?
end
def object_pool
diff --git a/app/models/project.rb b/app/models/project.rb
index 0711cd4aabd..e4b8db860a4 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2004,6 +2004,10 @@ class Project < ActiveRecord::Base
Feature.enabled?(:object_pools, self)
end
+ def leave_pool_repository
+ pool_repository&.unlink_repository(repository)
+ end
+
private
def create_new_pool_repository
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 07a13c33b89..4a7d13915dd 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -23,6 +23,10 @@ class EnvironmentEntity < Grape::Entity
stop_project_environment_path(environment.project, environment)
end
+ expose :cluster_type, if: ->(environment, _) { cluster_platform_kubernetes? } do |environment|
+ cluster.cluster_type
+ end
+
expose :terminal_path, if: ->(*) { environment.has_terminals? && can_access_terminal? } do |environment|
terminal_project_environment_path(environment.project, environment)
end
@@ -48,4 +52,16 @@ class EnvironmentEntity < Grape::Entity
def can_access_terminal?
can?(request.current_user, :create_environment_terminal, environment)
end
+
+ def cluster_platform_kubernetes?
+ deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes)
+ end
+
+ def deployment_platform
+ environment.deployment_platform
+ end
+
+ def cluster
+ deployment_platform.cluster
+ end
end
diff --git a/app/services/deploy_keys/create_service.rb b/app/services/deploy_keys/create_service.rb
index a25e73666f8..0c935285657 100644
--- a/app/services/deploy_keys/create_service.rb
+++ b/app/services/deploy_keys/create_service.rb
@@ -2,7 +2,7 @@
module DeployKeys
class CreateService < Keys::BaseService
- def execute
+ def execute(project: nil)
DeployKey.create(params.merge(user: user))
end
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 210571b6b4e..336d029d330 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -137,6 +137,8 @@ module Projects
raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.')
end
+ project.leave_pool_repository
+
Project.transaction do
log_destroy_event
trash_repositories!
diff --git a/app/views/projects/releases/index.html.haml b/app/views/projects/releases/index.html.haml
index 7bc942a3c3c..f01d4e826b9 100644
--- a/app/views/projects/releases/index.html.haml
+++ b/app/views/projects/releases/index.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
- page_title _('Releases')
-%div{ 'class' => container_class }
- #js-releases-page
+%div{ class: container_class }
+ #js-releases-page{ data: { project_id: @project.id, illustration_path: image_path('illustrations/releases.svg'), documentation_path: help_page_path('user/project/releases') } }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index bc26b3f8ef2..f9928362290 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -88,6 +88,7 @@
- object_pool:object_pool_create
- object_pool:object_pool_schedule_join
- object_pool:object_pool_join
+- object_pool:object_pool_destroy
- default
- mailers # ActionMailer::DeliveryJob.queue_name
diff --git a/app/workers/object_pool/destroy_worker.rb b/app/workers/object_pool/destroy_worker.rb
new file mode 100644
index 00000000000..ca00d467d9b
--- /dev/null
+++ b/app/workers/object_pool/destroy_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module ObjectPool
+ class DestroyWorker
+ include ApplicationWorker
+ include ObjectPoolQueue
+
+ def perform(pool_repository_id)
+ pool = PoolRepository.find_by_id(pool_repository_id)
+ return unless pool&.obsolete?
+
+ pool.delete_object_pool
+ pool.destroy
+ end
+ end
+end
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index 98c81956cba..f34ed6c4844 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -4,6 +4,10 @@ class StuckMergeJobsWorker
include ApplicationWorker
include CronjobQueue
+ def self.logger
+ Rails.logger
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def perform
stuck_merge_requests.find_in_batches(batch_size: 100) do |group|
@@ -35,7 +39,7 @@ class StuckMergeJobsWorker
# We rely on state machine callbacks to update head_pipeline_id
merge_requests_to_reopen.each(&:unlock_mr)
- Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")
+ self.class.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/changelogs/unreleased/23367-clarify-docs-allow-failure.yml b/changelogs/unreleased/23367-clarify-docs-allow-failure.yml
new file mode 100644
index 00000000000..221d9e83ffb
--- /dev/null
+++ b/changelogs/unreleased/23367-clarify-docs-allow-failure.yml
@@ -0,0 +1,5 @@
+---
+title: Clarifies docs about CI `allow_failure`
+merge_request: 23367
+author: C.J. Jameson
+type: other
diff --git a/changelogs/unreleased/41766-vuex-store.yml b/changelogs/unreleased/41766-vuex-store.yml
new file mode 100644
index 00000000000..f20fc736a6f
--- /dev/null
+++ b/changelogs/unreleased/41766-vuex-store.yml
@@ -0,0 +1,5 @@
+---
+title: Creates frontend app for releases
+merge_request: 23796
+author:
+type: added
diff --git a/changelogs/unreleased/51606-expanding-a-diff-while-having-an-open-comment-form-will-always-scroll-down-to-the-comment.yml b/changelogs/unreleased/51606-expanding-a-diff-while-having-an-open-comment-form-will-always-scroll-down-to-the-comment.yml
new file mode 100644
index 00000000000..a845234b42f
--- /dev/null
+++ b/changelogs/unreleased/51606-expanding-a-diff-while-having-an-open-comment-form-will-always-scroll-down-to-the-comment.yml
@@ -0,0 +1,5 @@
+---
+title: Stop autofocusing on diff comment after initial mount
+merge_request: 23849
+author:
+type: fixed
diff --git a/changelogs/unreleased/54814-sidebar-styling-updates.yml b/changelogs/unreleased/54814-sidebar-styling-updates.yml
new file mode 100644
index 00000000000..98e3836ee14
--- /dev/null
+++ b/changelogs/unreleased/54814-sidebar-styling-updates.yml
@@ -0,0 +1,5 @@
+---
+title: Fix label and header styles in the job details sidebar.
+merge_request: 23816
+author: Nathan Friend
+type: changed
diff --git a/changelogs/unreleased/54953-fix-commit_email_hostname-accessor-in-fake_application_settings.yml b/changelogs/unreleased/54953-fix-commit_email_hostname-accessor-in-fake_application_settings.yml
new file mode 100644
index 00000000000..623b3a7319c
--- /dev/null
+++ b/changelogs/unreleased/54953-fix-commit_email_hostname-accessor-in-fake_application_settings.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a 500 error that could occur until all migrations are done
+merge_request: 23939
+author:
+type: fixed
diff --git a/changelogs/unreleased/55103-hide-group-cluster-features.yml b/changelogs/unreleased/55103-hide-group-cluster-features.yml
new file mode 100644
index 00000000000..fbe780d6f01
--- /dev/null
+++ b/changelogs/unreleased/55103-hide-group-cluster-features.yml
@@ -0,0 +1,5 @@
+---
+title: Hide cluster features that don't work yet with Group Clusters
+merge_request: 23935
+author:
+type: fixed
diff --git a/changelogs/unreleased/depracated-migration-inheritance.yml b/changelogs/unreleased/depracated-migration-inheritance.yml
new file mode 100644
index 00000000000..1ea9b2df59c
--- /dev/null
+++ b/changelogs/unreleased/depracated-migration-inheritance.yml
@@ -0,0 +1,5 @@
+---
+title: ActiveRecord::Migration -> ActiveRecord::Migration[5.0]
+merge_request: 23910
+author: Jasper Maes
+type: other
diff --git a/changelogs/unreleased/mk-avoid-read-only-error.yml b/changelogs/unreleased/mk-avoid-read-only-error.yml
new file mode 100644
index 00000000000..8641f5db9f0
--- /dev/null
+++ b/changelogs/unreleased/mk-avoid-read-only-error.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent admins from attempting hashed storage migration on read only DB
+merge_request: 23597
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-cache-avatar-paths.yml b/changelogs/unreleased/sh-cache-avatar-paths.yml
new file mode 100644
index 00000000000..b59a4db413d
--- /dev/null
+++ b/changelogs/unreleased/sh-cache-avatar-paths.yml
@@ -0,0 +1,5 @@
+---
+title: Cache avatar URLs and paths within a request
+merge_request: 23950
+author:
+type: performance
diff --git a/changelogs/unreleased/winh-upgrade-gitlab-ui.yml b/changelogs/unreleased/winh-upgrade-gitlab-ui.yml
new file mode 100644
index 00000000000..b312a329f5d
--- /dev/null
+++ b/changelogs/unreleased/winh-upgrade-gitlab-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade @gitlab/ui to 1.16.2
+merge_request: 23946
+author:
+type: other
diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile
index be7b301866d..52af837c261 100644
--- a/danger/documentation/Dangerfile
+++ b/danger/documentation/Dangerfile
@@ -32,7 +32,7 @@ to be reviewed.
| Tech writer | Stage(s) |
| ------------ | ------------------------------------------------------------ |
| `@marcia` | ~Create ~Release + ~"development guidelines" |
-| `@axil` | ~Distribution ~Gitaly ~Gitter ~Monitoring ~Packaging ~Secure |
+| `@axil` | ~Distribution ~Gitaly ~Gitter ~Monitoring ~Package ~Secure |
| `@eread` | ~Manage ~Configure ~Geo ~Verify |
| `@mikelewis` | ~Plan |
diff --git a/db/migrate/20181006004100_import_common_metrics_nginx_vts.rb b/db/migrate/20181006004100_import_common_metrics_nginx_vts.rb
index 98fafed7912..5cd312837df 100644
--- a/db/migrate/20181006004100_import_common_metrics_nginx_vts.rb
+++ b/db/migrate/20181006004100_import_common_metrics_nginx_vts.rb
@@ -1,4 +1,4 @@
-class ImportCommonMetricsNginxVts < ActiveRecord::Migration
+class ImportCommonMetricsNginxVts < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
require Rails.root.join('db/importers/common_metrics_importer.rb')
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 6083806af6c..89132cd95f0 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -95,6 +95,7 @@ created in snippets, wikis, and repos.
- [Postfix for incoming email](reply_by_email_postfix_setup.md): Set up a
basic Postfix mail server with IMAP authentication on Ubuntu for incoming
emails.
+- [Abuse reports](../user/admin_area/abuse_reports.md): View and resolve abuse reports from your users.
[reply by email]: reply_by_email.md
[issues by email]: ../user/project/issues/create_new_issue.md#new-issue-via-email
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 589c48ee08d..d2dd9c676e3 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -8,12 +8,12 @@ Get a list of jobs in a project.
GET /projects/:id/jobs
```
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|---------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, `manual`; showing all jobs if none provided |
+| Attribute | Type | Required | Description |
+|-----------|--------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `scope` | string **or** array of strings | no | Scope of jobs to show. Either one of or an array of the following: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, or `manual`. All jobs are returned if `scope` is not provided. |
-```
+```sh
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope[]=pending&scope[]=running'
```
@@ -140,13 +140,13 @@ Get a list of jobs for a pipeline.
GET /projects/:id/pipelines/:pipeline_id/jobs
```
-| Attribute | Type | Required | Description |
-|---------------|--------------------------------|----------|----------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `pipeline_id` | integer | yes | The ID of a pipeline |
-| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, `manual`; showing all jobs if none provided |
+| Attribute | Type | Required | Description |
+|---------------|--------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `pipeline_id` | integer | yes | The ID of a pipeline. |
+| `scope` | string **or** array of strings | no | Scope of jobs to show. Either one of or an array of the following: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, or `manual`. All jobs are returned if `scope` is not provided. |
-```
+```sh
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/pipelines/6/jobs?scope[]=pending&scope[]=running'
```
@@ -273,12 +273,12 @@ Get a single job of a project
GET /projects/:id/jobs/:job_id
```
-| Attribute | Type | Required | Description |
-|------------|---------|----------|---------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `job_id` | integer | yes | The ID of a job |
+| Attribute | Type | Required | Description |
+|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `job_id` | integer | yes | The ID of a job. |
-```
+```sh
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8"
```
@@ -348,18 +348,18 @@ Get job artifacts of a project.
GET /projects/:id/jobs/:job_id/artifacts
```
-| Attribute | Type | Required | Description |
-|------------|---------|----------|---------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `job_id` | integer | yes | The ID of a job |
+| Attribute | Type | Required | Description |
+|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `job_id` | integer | yes | The ID of a job. |
Example requests:
-```
+```sh
curl --location --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/artifacts"
```
-Response:
+Possible response status codes:
| Status | Description |
|-----------|---------------------------------|
@@ -383,19 +383,19 @@ GET /projects/:id/jobs/artifacts/:ref_name/download?job=name
Parameters
-| Attribute | Type | Required | Description |
-|-------------|---------|----------|-------------------------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `ref_name` | string | yes | The ref from a repository (can only be branch or tag name, not HEAD or SHA) |
-| `job` | string | yes | The name of the job |
+| Attribute | Type | Required | Description |
+|------------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `ref_name` | string | yes | Branch or tag name in repository. HEAD or SHA references are not supported. |
+| `job` | string | yes | The name of the job. |
Example requests:
-```
+```sh
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test"
```
-Example response:
+Possible response status codes:
| Status | Description |
|-----------|---------------------------------|
@@ -408,9 +408,9 @@ Example response:
> Introduced in GitLab 10.0
-Download a single artifact file from within the job's artifacts archive.
-
-Only a single file is going to be extracted from the archive and streamed to a client.
+Download a single artifact file from a job with a specified ID from within
+the job's artifacts archive. The file is extracted from the archive and
+streamed to the client.
```
GET /projects/:id/jobs/:job_id/artifacts/*artifact_path
@@ -418,19 +418,19 @@ GET /projects/:id/jobs/:job_id/artifacts/*artifact_path
Parameters
-| Attribute | Type | Required | Description |
-|-----------------|---------|----------|-------------------------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `job_id ` | integer | yes | The unique job identifier |
-| `artifact_path` | string | yes | Path to a file inside the artifacts archive |
+| Attribute | Type | Required | Description |
+|-----------------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `job_id ` | integer | yes | The unique job identifier. |
+| `artifact_path` | string | yes | Path to a file inside the artifacts archive. |
Example request:
-```
+```sh
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/5/artifacts/some/release/file.pdf"
```
-Example response:
+Possible response status codes:
| Status | Description |
|-----------|--------------------------------------|
@@ -481,16 +481,16 @@ Get a trace of a specific job of a project
GET /projects/:id/jobs/:job_id/trace
```
-| Attribute | Type | Required | Description |
-|------------|---------|----------|---------------------|
-| id | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| job_id | integer | yes | The ID of a job |
+| Attribute | Type | Required | Description |
+|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
+| id | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| job_id | integer | yes | The ID of a job. |
-```
+```sh
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/trace"
```
-Response:
+Possible response status codes:
| Status | Description |
|-----------|-----------------------------------|
@@ -505,12 +505,12 @@ Cancel a single job of a project
POST /projects/:id/jobs/:job_id/cancel
```
-| Attribute | Type | Required | Description |
-|------------|---------|----------|---------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `job_id` | integer | yes | The ID of a job |
+| Attribute | Type | Required | Description |
+|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `job_id` | integer | yes | The ID of a job. |
-```
+```sh
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/cancel"
```
@@ -553,12 +553,12 @@ Retry a single job of a project
POST /projects/:id/jobs/:job_id/retry
```
-| Attribute | Type | Required | Description |
-|------------|---------|----------|---------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `job_id` | integer | yes | The ID of a job |
+| Attribute | Type | Required | Description |
+|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `job_id` | integer | yes | The ID of a job. |
-```
+```sh
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/retry"
```
@@ -603,14 +603,14 @@ POST /projects/:id/jobs/:job_id/erase
Parameters
-| Attribute | Type | Required | Description |
-|-------------|---------|----------|---------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `job_id` | integer | yes | The ID of a job |
+| Attribute | Type | Required | Description |
+|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `job_id` | integer | yes | The ID of a job. |
Example of request
-```
+```sh
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/erase"
```
@@ -656,14 +656,14 @@ POST /projects/:id/jobs/:job_id/artifacts/keep
Parameters
-| Attribute | Type | Required | Description |
-|-------------|---------|----------|---------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `job_id` | integer | yes | The ID of a job |
+| Attribute | Type | Required | Description |
+|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `job_id` | integer | yes | The ID of a job. |
Example request:
-```
+```sh
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/artifacts/keep"
```
@@ -707,12 +707,12 @@ Triggers a manual action to start a job.
POST /projects/:id/jobs/:job_id/play
```
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|---------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `job_id` | integer | yes | The ID of a job |
+| Attribute | Type | Required | Description |
+|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `job_id` | integer | yes | The ID of a job. |
-```
+```sh
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/play"
```
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index 706e83abf44..bf1e61442d4 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -1,11 +1,12 @@
# Pipelines for merge requests
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/15310) in GitLab 11.6
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/15310) in GitLab 11.6.
-Usually, when a developer creates a new merge request, a pipeline runs on the
+Usually, when you create a new merge request, a pipeline runs on the
new change and checks if it's qualified to be merged into a target branch. This
pipeline should contain only necessary jobs for checking the new changes.
-For example, unit tests, lint checks, and Review Apps are often used in this cycle.
+For example, unit tests, lint checks, and [Review Apps](../review_apps/index.md)
+are often used in this cycle.
With pipelines for merge requests, you can design a specific pipeline structure
for merge requests. All you need to do is just adding `only: [merge_requests]` to
@@ -14,10 +15,10 @@ Every time, when developers create or update merge requests, a pipeline runs on
their new commits at every push to GitLab.
NOTE: **Note**:
-If you use both this feature and the [Merge When Pipeline Succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md)
-feature, pipelines for merge requests take precendence over the other regular pipelines.
+If you use both this feature and [Merge When Pipeline Succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md),
+pipelines for merge requests take precedence over the other regular pipelines.
-For example, consider a GitLab CI/CD configuration in .gitlab-ci.yml as follows:
+For example, consider the following [`.gitlab-ci.yml`](../yaml/README.md):
```yaml
build:
@@ -39,20 +40,18 @@ deploy:
script: ./deploy
```
-After a developer updated code in a merge request with whatever methods (e.g. `git push`),
-GitLab detects that the code is updated and create a new pipeline for the merge request.
+After the merge request is updated with new commits, GitLab detects that changes
+have occurred and creates a new pipeline for the merge request.
The pipeline fetches the latest code from the source branch and run tests against it.
-In this example, the pipeline contains only `build` and `test` jobs.
-Since `deploy` job does not have the `only: [merge_requests]` rule,
+In the above example, the pipeline contains only `build` and `test` jobs.
+Since the `deploy` job doesn't have the `only: [merge_requests]` rule,
deployment jobs will not happen in the merge request.
-Consider this pipeline list viewed from the **Pipelines** tab in a merge request:
+Pipelines tagged as **merge request** indicate that they were triggered
+when a merge request was created or updated.
![Merge request page](img/merge_request.png)
-Note that pipelines tagged as **merge request** indicate that they were triggered
-when a merge request was created or updated.
-
The same tag is shown on the pipeline's details:
![Pipeline's details](img/pipeline_detail.png)
@@ -66,7 +65,7 @@ flow, external contributors follow the following steps:
1. Create a merge request from the forked project that targets the `master` branch
in the parent project.
1. A pipeline runs on the merge request.
-1. A mainatiner from the parent project checks the pipeline result, and merge
+1. A maintainer from the parent project checks the pipeline result, and merge
into a target branch if the latest pipeline has passed.
Currently, those pipelines are created in a **forked** project, not in the
@@ -77,7 +76,7 @@ by tweaking their GitLab Runner in the forked project.
There are multiple reasons about why GitLab doesn't allow those pipelines to be
created in the parent project, but one of the biggest reasons is security concern.
External users could steal secret variables from the parent project by modifying
-.gitlab-ci.yml, which could be some sort of credentials. This should not happen.
+`.gitlab-ci.yml`, which could be some sort of credentials. This should not happen.
We're discussing a secure solution of running pipelines for merge requests
that submitted from forked projects,
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 4df5d8fb54a..440254e58bd 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -584,15 +584,17 @@ osx job:
## `allow_failure`
-`allow_failure` is used when you want to allow a job to fail without impacting
-the rest of the CI suite. Failed jobs don't contribute to the commit status.
+`allow_failure` allows a job to fail without impacting the rest of the CI
+suite.
The default value is `false`, except for [manual](#whenmanual) jobs.
-When enabled and the job fails, the pipeline will be successful/green for all
-intents and purposes, but a "CI build passed with warnings" message will be
-displayed on the merge request or commit or job page. This is to be used by
-jobs that are allowed to fail, but where failure indicates some other (manual)
-steps should be taken elsewhere.
+When enabled and the job fails, the job will show an orange warning in the UI.
+However, the logical flow of the pipeline will consider the job a
+success/passed, and is not blocked.
+
+Assuming all other jobs are successful, the job's stage and its pipeline will
+show the same orange warning. However, the associated commit will be marked
+"passed", without warnings.
In the example below, `job1` and `job2` will run in parallel, but if `job1`
fails, it will not stop the next stage from running, since it's marked with
@@ -624,7 +626,8 @@ failure.
`when` can be set to one of the following values:
1. `on_success` - execute job only when all jobs from prior stages
- succeed. This is the default.
+ succeed (or are considered succeeding because they are marked
+ `allow_failure`). This is the default.
1. `on_failure` - execute job only when at least one job from prior stages
fails.
1. `always` - execute job regardless of the status of jobs from prior stages.
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index d9ae3c08172..e570627bfc1 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -185,7 +185,26 @@ your terminal (replacing `gitlab.com` with your GitLab's instance domain):
ssh -T git@gitlab.com
```
-You should receive a _Welcome to GitLab, `@username`!_ message.
+The first time you connect to GitLab via SSH, you will be asked to verify the
+authenticity of the GitLab host you are connecting to.
+For example, when connecting to GitLab.com, answer `yes` to add GitLab.com to
+the list of trusted hosts:
+
+```
+The authenticity of host 'gitlab.com (35.231.145.151)' can't be established.
+ECDSA key fingerprint is SHA256:HbW3g8zUjNSksFbqTiUWPWg2Bq1x8xdGUrliXFzSnUw.
+Are you sure you want to continue connecting (yes/no)? yes
+Warning: Permanently added 'gitlab.com' (ECDSA) to the list of known hosts.
+```
+
+NOTE: **Note:**
+For GitLab.com, consult the
+[SSH host keys fingerprints](../user/gitlab_com/index.md#ssh-host-keys-fingerprints),
+to make sure you're connecting to the correct server.
+
+Once added to the list of known hosts, you won't be asked to validate the
+authenticity of GitLab's host again. Run the above command once more, and
+you should only receive a _Welcome to GitLab, `@username`!_ message.
If the welcome message doesn't appear, run SSH's verbose mode by replacing `-T`
with `-vvvT` to understand where the error is.
diff --git a/doc/user/abuse_reports.md b/doc/user/abuse_reports.md
new file mode 100644
index 00000000000..1f4f598b121
--- /dev/null
+++ b/doc/user/abuse_reports.md
@@ -0,0 +1,53 @@
+# Abuse reports
+
+Report abuse from users to GitLab administrators.
+
+You can report a user through their:
+
+- [Profile](#reporting-abuse-through-a-users-profile)
+- [Comments](#reporting-abuse-through-a-users-comment)
+- [Issues and Merge requests](#reporting-abuse-through-a-users-issue-or-merge-request)
+
+## Reporting abuse through a user's profile
+
+To report abuse from a user's profile page:
+
+1. Click on the exclamation point report abuse button at the top right of the user's profile.
+1. Complete an abuse report.
+1. Click the **Send report** button.
+
+## Reporting abuse through a user's comment
+
+To report abuse from a user's comment:
+
+1. Click on the vertical ellipsis (â‹®) more actions button to open the dropdown.
+1. Select **Report as abuse**.
+1. Complete an abuse report.
+1. Click the **Send report** button.
+
+
+NOTE: **Note:**
+A URL to the reported user's comment will be
+pre-filled in the abuse report's **Message** field.
+
+## Reporting abuse through a user's issue or merge request
+
+The **Report abuse** button is displayed at the top right of the issue or merge request:
+
+- When **Report abuse** is selected from the menu that appears when the **Close issue** or **Close merge request** button is clicked, for users that have permission to close the issue or merge request.
+- When viewing the issue or merge request, for users that don't have permission to close the issue or merge request.
+
+With the **Report abuse** button displayed, to submit an abuse report:
+
+1. Click the **Report abuse** button.
+1. Submit an abuse report.
+1. Click the **Send report** button.
+
+NOTE: **Note:**
+A URL to the reported user's issue or merge request will be pre-filled
+in the abuse report's **Message** field.
+
+## Managing abuse reports
+
+Admins are able to view and resolve abuse reports.
+For more information, see [abuse reports administration documentation](admin_area/abuse_reports.md).
diff --git a/doc/user/admin_area/abuse_reports.md b/doc/user/admin_area/abuse_reports.md
new file mode 100644
index 00000000000..01c2d9607f5
--- /dev/null
+++ b/doc/user/admin_area/abuse_reports.md
@@ -0,0 +1,31 @@
+# Abuse reports
+
+View and resolve abuse reports from GitLab users.
+
+Admins can view abuse reports in the admin area and are able to
+resolve the reports by removing the reported user, blocking the reported user, or removing the report.
+
+## Reporting abuse
+
+To find out more about reporting abuse, see [abuse reports user documentation](../abuse_reports.md).
+
+## Resolving abuse reports
+
+To access abuse reports, go to **Admin area > Abuse Reports**.
+
+There are 3 ways to resolve an abuse report, with a button for each method:
+
+- Remove user & report: [Deletes the reported user](../profile/account/delete_account.md) from the instance and removes the abuse report from the list.
+- Block user: Blocks the reported user from the instance and does not remove the abuse report from the list.
+- Remove report: Removes the abuse report from the list and does not restrict the access for the reported user.
+
+![abuse-reports-page-image](img/abuse_reports_page.png)
+
+## Blocked users
+
+Blocking a user will not remove the abuse report from the list.
+
+Instead, the block button will be disabled and explain that the user is "Already blocked".
+You are still able to remove the user and/or report if necessary.
+
+![abuse-report-blocked-user-image](img/abuse_report_blocked_user.png)
diff --git a/doc/user/admin_area/img/abuse_report_blocked_user.png b/doc/user/admin_area/img/abuse_report_blocked_user.png
new file mode 100644
index 00000000000..0cb4c7bb8ac
--- /dev/null
+++ b/doc/user/admin_area/img/abuse_report_blocked_user.png
Binary files differ
diff --git a/doc/user/admin_area/img/abuse_reports_page.png b/doc/user/admin_area/img/abuse_reports_page.png
new file mode 100644
index 00000000000..81dbe976cda
--- /dev/null
+++ b/doc/user/admin_area/img/abuse_reports_page.png
Binary files differ
diff --git a/doc/user/index.md b/doc/user/index.md
index 08995032cb1..fc68404d0c2 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -113,6 +113,7 @@ methods available in GitLab.
user type (guest, reporter, developer, maintainer, owner).
- [Feature highlight](feature_highlight.md): Learn more about the little blue dots
around the app that explain certain features
+- [Abuse reports](abuse_reports.md): Report abuse from users to GitLab administrators
## Groups
diff --git a/doc/user/profile/account/delete_account.md b/doc/user/profile/account/delete_account.md
index 49f0ce2cd79..b497cc414af 100644
--- a/doc/user/profile/account/delete_account.md
+++ b/doc/user/profile/account/delete_account.md
@@ -25,7 +25,7 @@ Here's a list of things that will not be deleted:
Instead of being deleted, these records will be moved to a system-wide
"Ghost User", whose sole purpose is to act as a container for such records.
-When a user is deleted from an abuse report or spam log, these associated
+When a user is deleted from an [abuse report](../../admin_area/abuse_reports.md) or spam log, these associated
records are not ghosted and will be removed, along with any groups the user
is a sole owner of. Administrators can also request this behaviour when
deleting users from the [API](../../../api/users.md#user-deletion) or the
@@ -35,4 +35,3 @@ admin area.
[ce-10273]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10273
[ce-10467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10467
[ce-11853]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11853
-
diff --git a/doc/user/project/img/releases.png b/doc/user/project/img/releases.png
new file mode 100644
index 00000000000..aec1db89a75
--- /dev/null
+++ b/doc/user/project/img/releases.png
Binary files differ
diff --git a/doc/user/project/releases.md b/doc/user/project/releases.md
new file mode 100644
index 00000000000..8dad4240c91
--- /dev/null
+++ b/doc/user/project/releases.md
@@ -0,0 +1,12 @@
+# Releases
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41766) in GitLab 11.7.
+
+Releases mark specific points in a project's development history, communicate
+information about the type of change, and deliver on prepared, often compiled,
+versions of the software to be reused elsewhere. Currently, releases can only be
+created through the API.
+
+Navigate to **Project > Releases** in order to see the list of releases of a project.
+
+![Releases list](img/releases.png)
diff --git a/lib/gitlab/fake_application_settings.rb b/lib/gitlab/fake_application_settings.rb
index db1aeeea8d3..bd806269bf0 100644
--- a/lib/gitlab/fake_application_settings.rb
+++ b/lib/gitlab/fake_application_settings.rb
@@ -37,5 +37,9 @@ module Gitlab
def pick_repository_storage
repository_storages.sample
end
+
+ def commit_email_hostname
+ super.presence || ApplicationSetting.default_commit_email_hostname
+ end
end
end
diff --git a/lib/gitlab/git/object_pool.rb b/lib/gitlab/git/object_pool.rb
index ba9b40b7df4..1c6242b444a 100644
--- a/lib/gitlab/git/object_pool.rb
+++ b/lib/gitlab/git/object_pool.rb
@@ -8,7 +8,7 @@ module Gitlab
GL_REPOSITORY = ""
delegate :exists?, :size, to: :repository
- delegate :delete, to: :object_pool_service
+ delegate :unlink_repository, :delete, to: :object_pool_service
attr_reader :storage, :relative_path, :source_repository
diff --git a/lib/gitlab/gitaly_client/object_pool_service.rb b/lib/gitlab/gitaly_client/object_pool_service.rb
index 272ce73ad64..6e7ede5fd18 100644
--- a/lib/gitlab/gitaly_client/object_pool_service.rb
+++ b/lib/gitlab/gitaly_client/object_pool_service.rb
@@ -35,7 +35,10 @@ module Gitlab
end
def unlink_repository(repository)
- request = Gitaly::UnlinkRepositoryFromObjectPoolRequest.new(repository: repository.gitaly_repository)
+ request = Gitaly::UnlinkRepositoryFromObjectPoolRequest.new(
+ object_pool: object_pool,
+ repository: repository.gitaly_repository
+ )
GitalyClient.call(storage, :object_pool_service, :unlink_repository_from_object_pool,
request, timeout: GitalyClient.fast_timeout)
diff --git a/lib/gitlab/json_cache.rb b/lib/gitlab/json_cache.rb
new file mode 100644
index 00000000000..1adf83739ad
--- /dev/null
+++ b/lib/gitlab/json_cache.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class JsonCache
+ attr_reader :backend, :cache_key_with_version, :namespace
+
+ def initialize(options = {})
+ @backend = options.fetch(:backend, Rails.cache)
+ @namespace = options.fetch(:namespace, nil)
+ @cache_key_with_version = options.fetch(:cache_key_with_version, true)
+ end
+
+ def active?
+ if backend.respond_to?(:active?)
+ backend.active?
+ else
+ true
+ end
+ end
+
+ def cache_key(key)
+ expanded_cache_key = [namespace, key].compact
+
+ if cache_key_with_version
+ expanded_cache_key << Rails.version
+ end
+
+ expanded_cache_key.join(':')
+ end
+
+ def expire(key)
+ backend.delete(cache_key(key))
+ end
+
+ def read(key, klass = nil)
+ value = backend.read(cache_key(key))
+ value = parse_value(value, klass) if value
+ value
+ end
+
+ def write(key, value, options = nil)
+ backend.write(cache_key(key), value.to_json, options)
+ end
+
+ def fetch(key, options = {}, &block)
+ klass = options.delete(:as)
+ value = read(key, klass)
+
+ return value unless value.nil?
+
+ value = yield
+
+ write(key, value, options)
+
+ value
+ end
+
+ private
+
+ def parse_value(raw, klass)
+ value = ActiveSupport::JSON.decode(raw)
+
+ case value
+ when Hash then parse_entry(value, klass)
+ when Array then parse_entries(value, klass)
+ else
+ value
+ end
+ rescue ActiveSupport::JSON.parse_error
+ nil
+ end
+
+ def parse_entry(raw, klass)
+ klass.new(raw) if valid_entry?(raw, klass)
+ end
+
+ def valid_entry?(raw, klass)
+ return false unless klass && raw.is_a?(Hash)
+
+ (raw.keys - klass.attribute_names).empty?
+ end
+
+ def parse_entries(values, klass)
+ values.map { |value| parse_entry(value, klass) }.compact
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake
index f539b1df955..09dc3aa9882 100644
--- a/lib/tasks/gitlab/storage.rake
+++ b/lib/tasks/gitlab/storage.rake
@@ -2,6 +2,12 @@ namespace :gitlab do
namespace :storage do
desc 'GitLab | Storage | Migrate existing projects to Hashed Storage'
task migrate_to_hashed: :environment do
+ if Gitlab::Database.read_only?
+ warn 'This task requires database write access. Exiting.'
+
+ next
+ end
+
storage_migrator = Gitlab::HashedStorage::Migrator.new
helper = Gitlab::HashedStorage::RakeHelper
@@ -9,7 +15,7 @@ namespace :gitlab do
project = Project.with_unmigrated_storage.find_by(id: helper.range_from)
unless project
- puts "There are no projects requiring storage migration with ID=#{helper.range_from}"
+ warn "There are no projects requiring storage migration with ID=#{helper.range_from}"
next
end
@@ -23,7 +29,7 @@ namespace :gitlab do
legacy_projects_count = Project.with_unmigrated_storage.count
if legacy_projects_count == 0
- puts 'There are no projects requiring storage migration. Nothing to do!'
+ warn 'There are no projects requiring storage migration. Nothing to do!'
next
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 61fb56d2fa2..fed490309cc 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -525,6 +525,9 @@ msgstr ""
msgid "An error has occurred"
msgstr ""
+msgid "An error occured while fetching the releases. Please try again."
+msgstr ""
+
msgid "An error occurred creating the new branch."
msgstr ""
@@ -3172,6 +3175,9 @@ msgstr ""
msgid "Geo"
msgstr ""
+msgid "Getting started with releases"
+msgstr ""
+
msgid "Git"
msgstr ""
@@ -3756,7 +3762,7 @@ msgstr ""
msgid "Job|The artifacts were removed"
msgstr ""
-msgid "Job|The artifacts will be removed in"
+msgid "Job|The artifacts will be removed"
msgstr ""
msgid "Job|This job is stuck because the project doesn't have any runners online assigned to it."
@@ -4090,9 +4096,6 @@ msgstr ""
msgid "Merge Request"
msgstr ""
-msgid "Merge Request:"
-msgstr ""
-
msgid "Merge Requests"
msgstr ""
@@ -4628,6 +4631,9 @@ msgstr ""
msgid "Open"
msgstr ""
+msgid "Open Documentation"
+msgstr ""
+
msgid "Open in Xcode"
msgstr ""
@@ -5527,6 +5533,9 @@ msgstr ""
msgid "Releases"
msgstr ""
+msgid "Releases mark specific points in a project's development history, communicate information about the type of change, and deliver on prepared, often compiled, versions of the software to be reused elsewhere. Currently, releases can only be created through the API."
+msgstr ""
+
msgid "Remind later"
msgstr ""
@@ -7137,10 +7146,13 @@ msgstr ""
msgid "Trending"
msgstr ""
-msgid "Trigger"
+msgid "Trigger this manual action"
msgstr ""
-msgid "Trigger this manual action"
+msgid "Trigger token:"
+msgstr ""
+
+msgid "Trigger variables:"
msgstr ""
msgid "Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will impersonate their associated user including their access to projects and their project permissions."
@@ -7344,9 +7356,6 @@ msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
msgstr ""
-msgid "Variables:"
-msgstr ""
-
msgid "Various container registry settings."
msgstr ""
@@ -7876,6 +7885,9 @@ msgstr ""
msgid "importing"
msgstr ""
+msgid "in"
+msgstr ""
+
msgid "issue boards"
msgstr ""
diff --git a/package.json b/package.json
index 8ee31b8e57b..e445fb457a3 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"@babel/preset-env": "^7.1.0",
"@gitlab/csslab": "^1.8.0",
"@gitlab/svgs": "^1.43.0",
- "@gitlab/ui": "^1.15.0",
+ "@gitlab/ui": "^1.16.0",
"apollo-boost": "^0.1.20",
"apollo-client": "^2.4.5",
"autosize": "^4.0.0",
diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb
index a927d4329ef..e54cf3e8181 100644
--- a/spec/controllers/projects/deploy_keys_controller_spec.rb
+++ b/spec/controllers/projects/deploy_keys_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::DeployKeysController do
end
context 'when html requested' do
- it 'redirects to blob' do
+ it 'redirects to project settings with the correct anchor' do
get :index, params: params
expect(response).to redirect_to(project_settings_repository_path(project, anchor: 'js-deploy-keys-settings'))
@@ -60,6 +60,40 @@ describe Projects::DeployKeysController do
end
end
+ describe 'POST create' do
+ def create_params(title = 'my-key')
+ {
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ deploy_key: {
+ title: title,
+ key: attributes_for(:deploy_key)[:key],
+ deploy_keys_projects_attributes: { '0' => { can_push: '1' } }
+ }
+ }
+ end
+
+ it 'creates a new deploy key for the project' do
+ expect { post :create, params: create_params }.to change(project.deploy_keys, :count).by(1)
+
+ expect(response).to redirect_to(project_settings_repository_path(project, anchor: 'js-deploy-keys-settings'))
+ end
+
+ it 'redirects to project settings with the correct anchor' do
+ post :create, params: create_params
+
+ expect(response).to redirect_to(project_settings_repository_path(project, anchor: 'js-deploy-keys-settings'))
+ end
+
+ context 'when the deploy key is invalid' do
+ it 'shows an alert with the validations errors' do
+ post :create, params: create_params(nil)
+
+ expect(flash[:alert]).to eq("Title can't be blank, Deploy keys projects deploy key title can't be blank")
+ end
+ end
+ end
+
describe '/enable/:id' do
let(:deploy_key) { create(:deploy_key) }
let(:project2) { create(:project) }
diff --git a/spec/factories/pool_repositories.rb b/spec/factories/pool_repositories.rb
index 265a4643f46..36e54cf44b4 100644
--- a/spec/factories/pool_repositories.rb
+++ b/spec/factories/pool_repositories.rb
@@ -15,6 +15,10 @@ FactoryBot.define do
state :failed
end
+ trait :obsolete do
+ state :obsolete
+ end
+
trait :ready do
state :ready
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 651c02c7ecc..60f37f4b74a 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -351,8 +351,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context 'when user is a maintainer' do
shared_examples 'no reveal button variables behavior' do
it 'renders a hidden value with no reveal values button', :js do
- expect(page).to have_content('Token')
- expect(page).to have_content('Variables')
+ expect(page).to have_content('Trigger token')
+ expect(page).to have_content('Trigger variables')
expect(page).not_to have_css('.js-reveal-variables')
@@ -389,8 +389,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
shared_examples 'reveal button variables behavior' do
it 'renders a hidden value with a reveal values button', :js do
- expect(page).to have_content('Token')
- expect(page).to have_content('Variables')
+ expect(page).to have_content('Trigger token')
+ expect(page).to have_content('Trigger variables')
expect(page).to have_css('.js-reveal-variables')
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
index f096638e3d6..4268634d302 100644
--- a/spec/javascripts/diffs/store/utils_spec.js
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -294,10 +294,14 @@ describe('DiffsStoreUtils', () => {
});
describe('prepareDiffData', () => {
- it('sets the renderIt and collapsed attribute on files', () => {
- const preparedDiff = { diff_files: [getDiffFileMock()] };
+ let preparedDiff;
+
+ beforeEach(() => {
+ preparedDiff = { diff_files: [getDiffFileMock()] };
utils.prepareDiffData(preparedDiff);
+ });
+ it('sets the renderIt and collapsed attribute on files', () => {
const firstParallelDiffLine = preparedDiff.diff_files[0].parallel_diff_lines[2];
expect(firstParallelDiffLine.left.discussions.length).toBe(0);
@@ -323,6 +327,18 @@ describe('DiffsStoreUtils', () => {
expect(preparedDiff.diff_files[0].renderIt).toBeTruthy();
expect(preparedDiff.diff_files[0].collapsed).toBeFalsy();
});
+
+ it('adds line_code to all lines', () => {
+ expect(
+ preparedDiff.diff_files[0].parallel_diff_lines.filter(line => !line.line_code),
+ ).toHaveLength(0);
+ });
+
+ it('uses right line code if left has none', () => {
+ const firstLine = preparedDiff.diff_files[0].parallel_diff_lines[0];
+
+ expect(firstLine.line_code).toEqual(firstLine.right.line_code);
+ });
});
describe('isDiscussionApplicableToLine', () => {
diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js
index f1576b19d1b..56e18db59c5 100644
--- a/spec/javascripts/environments/environment_terminal_button_spec.js
+++ b/spec/javascripts/environments/environment_terminal_button_spec.js
@@ -2,30 +2,46 @@ import Vue from 'vue';
import terminalComp from '~/environments/components/environment_terminal_button.vue';
describe('Stop Component', () => {
- let TerminalComponent;
let component;
const terminalPath = '/path';
- beforeEach(() => {
- TerminalComponent = Vue.extend(terminalComp);
-
+ const mountWithProps = props => {
+ const TerminalComponent = Vue.extend(terminalComp);
component = new TerminalComponent({
- propsData: {
- terminalPath,
- },
+ propsData: props,
}).$mount();
- });
+ };
+
+ describe('enabled', () => {
+ beforeEach(() => {
+ mountWithProps({ terminalPath });
+ });
+
+ describe('computed', () => {
+ it('title', () => {
+ expect(component.title).toEqual('Terminal');
+ });
+ });
- describe('computed', () => {
- it('title', () => {
- expect(component.title).toEqual('Terminal');
+ it('should render a link to open a web terminal with the provided path', () => {
+ expect(component.$el.tagName).toEqual('A');
+ expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal');
+ expect(component.$el.getAttribute('aria-label')).toEqual('Terminal');
+ expect(component.$el.getAttribute('href')).toEqual(terminalPath);
+ });
+
+ it('should render a non-disabled button', () => {
+ expect(component.$el.classList).not.toContain('disabled');
});
});
- it('should render a link to open a web terminal with the provided path', () => {
- expect(component.$el.tagName).toEqual('A');
- expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal');
- expect(component.$el.getAttribute('aria-label')).toEqual('Terminal');
- expect(component.$el.getAttribute('href')).toEqual(terminalPath);
+ describe('disabled', () => {
+ beforeEach(() => {
+ mountWithProps({ terminalPath, disabled: true });
+ });
+
+ it('should render a disabled button', () => {
+ expect(component.$el.classList).toContain('disabled');
+ });
});
});
diff --git a/spec/javascripts/jobs/components/artifacts_block_spec.js b/spec/javascripts/jobs/components/artifacts_block_spec.js
index 2fa7ff653fe..27d480ef2ea 100644
--- a/spec/javascripts/jobs/components/artifacts_block_spec.js
+++ b/spec/javascripts/jobs/components/artifacts_block_spec.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
import component from '~/jobs/components/artifacts_block.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
+import { trimText } from '../../helpers/vue_component_helper';
describe('Artifacts block', () => {
const Component = Vue.extend(component);
@@ -9,7 +10,7 @@ describe('Artifacts block', () => {
const expireAt = '2018-08-14T09:38:49.157Z';
const timeago = getTimeago();
- const formatedDate = timeago.format(expireAt);
+ const formattedDate = timeago.format(expireAt);
const expiredArtifact = {
expire_at: expireAt,
@@ -36,9 +37,8 @@ describe('Artifacts block', () => {
expect(vm.$el.querySelector('.js-artifacts-removed')).not.toBeNull();
expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).toBeNull();
- expect(vm.$el.textContent).toContain(formatedDate);
- expect(vm.$el.querySelector('.js-artifacts-removed').textContent.trim()).toEqual(
- 'The artifacts were removed',
+ expect(trimText(vm.$el.querySelector('.js-artifacts-removed').textContent)).toEqual(
+ `The artifacts were removed ${formattedDate}`,
);
});
});
@@ -51,9 +51,8 @@ describe('Artifacts block', () => {
expect(vm.$el.querySelector('.js-artifacts-removed')).toBeNull();
expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).not.toBeNull();
- expect(vm.$el.textContent).toContain(formatedDate);
- expect(vm.$el.querySelector('.js-artifacts-will-be-removed').textContent.trim()).toEqual(
- 'The artifacts will be removed in',
+ expect(trimText(vm.$el.querySelector('.js-artifacts-will-be-removed').textContent)).toEqual(
+ `The artifacts will be removed ${formattedDate}`,
);
});
});
diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js
index 424092d2d88..b0bc16d7c64 100644
--- a/spec/javascripts/jobs/components/sidebar_spec.js
+++ b/spec/javascripts/jobs/components/sidebar_spec.js
@@ -79,14 +79,6 @@ describe('Sidebar details block', () => {
});
describe('information', () => {
- it('should render merge request link', () => {
- expect(trimText(vm.$el.querySelector('.js-job-mr').textContent)).toEqual('Merge Request: !2');
-
- expect(vm.$el.querySelector('.js-job-mr a').getAttribute('href')).toEqual(
- job.merge_request.path,
- );
- });
-
it('should render job duration', () => {
expect(trimText(vm.$el.querySelector('.js-job-duration').textContent)).toEqual(
'Duration: 6 seconds',
diff --git a/spec/javascripts/releases/components/app_spec.js b/spec/javascripts/releases/components/app_spec.js
new file mode 100644
index 00000000000..f30c7685e34
--- /dev/null
+++ b/spec/javascripts/releases/components/app_spec.js
@@ -0,0 +1,79 @@
+import Vue from 'vue';
+import app from '~/releases/components/app.vue';
+import createStore from '~/releases/store';
+import api from '~/api';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from '../store/helpers';
+import { releases } from '../mock_data';
+
+describe('Releases App ', () => {
+ const Component = Vue.extend(app);
+ let store;
+ let vm;
+
+ const props = {
+ projectId: 'gitlab-ce',
+ documentationLink: 'help/releases',
+ illustrationPath: 'illustration/path',
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ vm.$destroy();
+ });
+
+ describe('while loading', () => {
+ beforeEach(() => {
+ spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [] }));
+ vm = mountComponentWithStore(Component, { props, store });
+ });
+
+ it('renders loading icon', done => {
+ expect(vm.$el.querySelector('.js-loading')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
+ expect(vm.$el.querySelector('.js-success-state')).toBeNull();
+
+ setTimeout(() => {
+ done();
+ }, 0);
+ });
+ });
+
+ describe('with successful request', () => {
+ beforeEach(() => {
+ spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: releases }));
+ vm = mountComponentWithStore(Component, { props, store });
+ });
+
+ it('renders success state', done => {
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-loading')).toBeNull();
+ expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
+ expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
+
+ done();
+ }, 0);
+ });
+ });
+
+ describe('with empty request', () => {
+ beforeEach(() => {
+ spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [] }));
+ vm = mountComponentWithStore(Component, { props, store });
+ });
+
+ it('renders empty state', done => {
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-loading')).toBeNull();
+ expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-success-state')).toBeNull();
+
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/releases/components/release_block_spec.js b/spec/javascripts/releases/components/release_block_spec.js
index c0cd15b7507..19aecbb3636 100644
--- a/spec/javascripts/releases/components/release_block_spec.js
+++ b/spec/javascripts/releases/components/release_block_spec.js
@@ -28,6 +28,16 @@ describe('Release block', () => {
committer_name: 'Jack Smith',
committer_email: 'jack@example.com',
committed_date: '2012-05-28T04:42:42-07:00',
+ author: {
+ avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
+ id: 482476,
+ name: 'John Doe',
+ path: '/johndoe',
+ state: 'active',
+ status_tooltip_html: null,
+ username: 'johndoe',
+ web_url: 'https://gitlab.com/johndoe',
+ },
},
assets: {
count: 6,
@@ -66,32 +76,10 @@ describe('Release block', () => {
],
},
};
-
- const props = {
- name: release.name,
- tag: release.tag_name,
- commit: release.commit,
- description: release.description_html,
- author: {
- avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
- id: 482476,
- name: 'John Doe',
- path: '/johndoe',
- state: 'active',
- status_tooltip_html: null,
- username: 'johndoe',
- web_url: 'https://gitlab.com/johndoe',
- },
- createdAt: release.created_at,
- assetsCount: release.assets.count,
- sources: release.assets.sources,
- links: release.assets.links,
- };
-
let vm;
beforeEach(() => {
- vm = mountComponent(Component, props);
+ vm = mountComponent(Component, { release });
});
afterEach(() => {
diff --git a/spec/javascripts/releases/mock_data.js b/spec/javascripts/releases/mock_data.js
new file mode 100644
index 00000000000..2855eca1711
--- /dev/null
+++ b/spec/javascripts/releases/mock_data.js
@@ -0,0 +1,128 @@
+export const release = {
+ name: 'Bionic Beaver',
+ tag_name: '18.04',
+ description: '## changelog\n\n* line 1\n* line2',
+ description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
+ author_name: 'Release bot',
+ author_email: 'release-bot@example.com',
+ created_at: '2012-05-28T05:00:00-07:00',
+ commit: {
+ id: '2695effb5807a22ff3d138d593fd856244e155e7',
+ short_id: '2695effb',
+ title: 'Initial commit',
+ created_at: '2017-07-26T11:08:53.000+02:00',
+ parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'],
+ message: 'Initial commit',
+ author: {
+ avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
+ id: 482476,
+ name: 'John Doe',
+ path: '/johndoe',
+ state: 'active',
+ status_tooltip_html: null,
+ username: 'johndoe',
+ web_url: 'https://gitlab.com/johndoe',
+ },
+ authored_date: '2012-05-28T04:42:42-07:00',
+ committer_name: 'Jack Smith',
+ committer_email: 'jack@example.com',
+ committed_date: '2012-05-28T04:42:42-07:00',
+ },
+ assets: {
+ count: 6,
+ sources: [
+ {
+ format: 'zip',
+ url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip',
+ },
+ {
+ format: 'tar.gz',
+ url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz',
+ },
+ {
+ format: 'tar.bz2',
+ url:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2',
+ },
+ {
+ format: 'tar',
+ url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar',
+ },
+ ],
+ links: [
+ {
+ name: 'release-18.04.dmg',
+ url: 'https://my-external-hosting.example.com/scrambled-url/',
+ external: true,
+ },
+ {
+ name: 'binary-linux-amd64',
+ url:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
+ external: false,
+ },
+ ],
+ },
+};
+
+export const releases = [
+ release,
+ {
+ name: 'JoJos Bizarre Adventure',
+ tag_name: '19.00',
+ description: '## changelog\n\n* line 1\n* line2',
+ description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
+ author_name: 'Release bot',
+ author_email: 'release-bot@example.com',
+ created_at: '2012-05-28T05:00:00-07:00',
+ commit: {
+ id: '2695effb5807a22ff3d138d593fd856244e155e7',
+ short_id: '2695effb',
+ title: 'Initial commit',
+ created_at: '2017-07-26T11:08:53.000+02:00',
+ parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'],
+ message: 'Initial commit',
+ author: {
+ avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
+ id: 482476,
+ name: 'John Doe',
+ path: '/johndoe',
+ state: 'active',
+ status_tooltip_html: null,
+ username: 'johndoe',
+ web_url: 'https://gitlab.com/johndoe',
+ },
+ authored_date: '2012-05-28T04:42:42-07:00',
+ committer_name: 'Jack Smith',
+ committer_email: 'jack@example.com',
+ committed_date: '2012-05-28T04:42:42-07:00',
+ },
+ assets: {
+ count: 4,
+ sources: [
+ {
+ format: 'tar.gz',
+ url:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz',
+ },
+ {
+ format: 'tar.bz2',
+ url:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2',
+ },
+ {
+ format: 'tar',
+ url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar',
+ },
+ ],
+ links: [
+ {
+ name: 'binary-linux-amd64',
+ url:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
+ external: false,
+ },
+ ],
+ },
+ },
+];
diff --git a/spec/javascripts/releases/store/actions_spec.js b/spec/javascripts/releases/store/actions_spec.js
new file mode 100644
index 00000000000..6eb8e681be9
--- /dev/null
+++ b/spec/javascripts/releases/store/actions_spec.js
@@ -0,0 +1,98 @@
+import {
+ requestReleases,
+ fetchReleases,
+ receiveReleasesSuccess,
+ receiveReleasesError,
+} from '~/releases/store/actions';
+import state from '~/releases/store/state';
+import * as types from '~/releases/store/mutation_types';
+import api from '~/api';
+import testAction from 'spec/helpers/vuex_action_helper';
+import { releases } from '../mock_data';
+
+describe('Releases State actions', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('requestReleases', () => {
+ it('should commit REQUEST_RELEASES mutation', done => {
+ testAction(requestReleases, null, mockedState, [{ type: types.REQUEST_RELEASES }], [], done);
+ });
+ });
+
+ describe('fetchReleases', () => {
+ describe('success', () => {
+ it('dispatches requestReleases and receiveReleasesSuccess ', done => {
+ spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: releases }));
+
+ testAction(
+ fetchReleases,
+ releases,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestReleases',
+ },
+ {
+ payload: releases,
+ type: 'receiveReleasesSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ it('dispatches requestReleases and receiveReleasesError ', done => {
+ spyOn(api, 'releases').and.returnValue(Promise.reject());
+
+ testAction(
+ fetchReleases,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestReleases',
+ },
+ {
+ type: 'receiveReleasesError',
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveReleasesSuccess', () => {
+ it('should commit RECEIVE_RELEASES_SUCCESS mutation', done => {
+ testAction(
+ receiveReleasesSuccess,
+ releases,
+ mockedState,
+ [{ type: types.RECEIVE_RELEASES_SUCCESS, payload: releases }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveReleasesError', () => {
+ it('should commit RECEIVE_RELEASES_ERROR mutation', done => {
+ testAction(
+ receiveReleasesError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_RELEASES_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/releases/store/helpers.js b/spec/javascripts/releases/store/helpers.js
new file mode 100644
index 00000000000..e962b254377
--- /dev/null
+++ b/spec/javascripts/releases/store/helpers.js
@@ -0,0 +1,6 @@
+import state from '~/releases/store/state';
+
+// eslint-disable-next-line import/prefer-default-export
+export const resetStore = store => {
+ store.replaceState(state());
+};
diff --git a/spec/javascripts/releases/store/mutations_spec.js b/spec/javascripts/releases/store/mutations_spec.js
new file mode 100644
index 00000000000..72b98529fe9
--- /dev/null
+++ b/spec/javascripts/releases/store/mutations_spec.js
@@ -0,0 +1,47 @@
+import state from '~/releases/store/state';
+import mutations from '~/releases/store/mutations';
+import * as types from '~/releases/store/mutation_types';
+import { releases } from '../mock_data';
+
+describe('Releases Store Mutations', () => {
+ let stateCopy;
+
+ beforeEach(() => {
+ stateCopy = state();
+ });
+
+ describe('REQUEST_RELEASES', () => {
+ it('sets isLoading to true', () => {
+ mutations[types.REQUEST_RELEASES](stateCopy);
+
+ expect(stateCopy.isLoading).toEqual(true);
+ });
+ });
+
+ describe('RECEIVE_RELEASES_SUCCESS', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, releases);
+ });
+
+ it('sets is loading to false', () => {
+ expect(stateCopy.isLoading).toEqual(false);
+ });
+
+ it('sets hasError to false', () => {
+ expect(stateCopy.hasError).toEqual(false);
+ });
+
+ it('sets data', () => {
+ expect(stateCopy.releases).toEqual(releases);
+ });
+ });
+
+ describe('RECEIVE_RELEASES_ERROR', () => {
+ it('resets data', () => {
+ mutations[types.RECEIVE_RELEASES_ERROR](stateCopy);
+
+ expect(stateCopy.isLoading).toEqual(false);
+ expect(stateCopy.releases).toEqual([]);
+ });
+ });
+});
diff --git a/spec/lib/gitlab/json_cache_spec.rb b/spec/lib/gitlab/json_cache_spec.rb
new file mode 100644
index 00000000000..b52078e8556
--- /dev/null
+++ b/spec/lib/gitlab/json_cache_spec.rb
@@ -0,0 +1,401 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::JsonCache do
+ let(:backend) { double('backend').as_null_object }
+ let(:namespace) { 'geo' }
+ let(:key) { 'foo' }
+ let(:expanded_key) { "#{namespace}:#{key}:#{Rails.version}" }
+ let(:broadcast_message) { create(:broadcast_message) }
+
+ subject(:cache) { described_class.new(namespace: namespace, backend: backend) }
+
+ describe '#active?' do
+ context 'when backend respond to active? method' do
+ it 'delegates to the underlying cache implementation' do
+ backend = double('backend', active?: false)
+
+ cache = described_class.new(namespace: namespace, backend: backend)
+
+ expect(cache.active?).to eq(false)
+ end
+ end
+
+ context 'when backend does not respond to active? method' do
+ it 'returns true' do
+ backend = double('backend')
+
+ cache = described_class.new(namespace: namespace, backend: backend)
+
+ expect(cache.active?).to eq(true)
+ end
+ end
+ end
+
+ describe '#cache_key' do
+ context 'when namespace is not defined' do
+ it 'expands out the key with Rails version' do
+ cache = described_class.new(cache_key_with_version: true)
+
+ cache_key = cache.cache_key(key)
+
+ expect(cache_key).to eq("#{key}:#{Rails.version}")
+ end
+ end
+
+ context 'when cache_key_with_version is true' do
+ it 'expands out the key with namespace and Rails version' do
+ cache = described_class.new(namespace: namespace, cache_key_with_version: true)
+
+ cache_key = cache.cache_key(key)
+
+ expect(cache_key).to eq("#{namespace}:#{key}:#{Rails.version}")
+ end
+ end
+
+ context 'when cache_key_with_version is false' do
+ it 'expands out the key with namespace' do
+ cache = described_class.new(namespace: namespace, cache_key_with_version: false)
+
+ cache_key = cache.cache_key(key)
+
+ expect(cache_key).to eq("#{namespace}:#{key}")
+ end
+ end
+
+ context 'when namespace is nil, and cache_key_with_version is false' do
+ it 'returns the key' do
+ cache = described_class.new(namespace: nil, cache_key_with_version: false)
+
+ cache_key = cache.cache_key(key)
+
+ expect(cache_key).to eq(key)
+ end
+ end
+ end
+
+ describe '#expire' do
+ it 'expires the given key from the cache' do
+ cache.expire(key)
+
+ expect(backend).to have_received(:delete).with(expanded_key)
+ end
+ end
+
+ describe '#read' do
+ it 'reads the given key from the cache' do
+ cache.read(key)
+
+ expect(backend).to have_received(:read).with(expanded_key)
+ end
+
+ it 'returns the cached value when there is data in the cache with the given key' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return("true")
+
+ expect(cache.read(key)).to eq(true)
+ end
+
+ it 'returns nil when there is no data in the cache with the given key' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return(nil)
+
+ expect(cache.read(key)).to be_nil
+ end
+
+ context 'when the cached value is a hash' do
+ it 'parses the cached value' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return(broadcast_message.to_json)
+
+ expect(cache.read(key, BroadcastMessage)).to eq(broadcast_message)
+ end
+
+ it 'returns nil when klass is nil' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return(broadcast_message.to_json)
+
+ expect(cache.read(key)).to be_nil
+ end
+
+ it 'gracefully handles bad cached entry' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return('{')
+
+ expect(cache.read(key, BroadcastMessage)).to be_nil
+ end
+
+ it 'gracefully handles an empty hash' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return('{}')
+
+ expect(cache.read(key, BroadcastMessage)).to be_a(BroadcastMessage)
+ end
+
+ it 'gracefully handles unknown attributes' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return(broadcast_message.attributes.merge(unknown_attribute: 1).to_json)
+
+ expect(cache.read(key, BroadcastMessage)).to be_nil
+ end
+ end
+
+ context 'when the cached value is an array' do
+ it 'parses the cached value' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return([broadcast_message].to_json)
+
+ expect(cache.read(key, BroadcastMessage)).to eq([broadcast_message])
+ end
+
+ it 'returns an empty array when klass is nil' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return([broadcast_message].to_json)
+
+ expect(cache.read(key)).to eq([])
+ end
+
+ it 'gracefully handles bad cached entry' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return('[')
+
+ expect(cache.read(key, BroadcastMessage)).to be_nil
+ end
+
+ it 'gracefully handles an empty array' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return('[]')
+
+ expect(cache.read(key, BroadcastMessage)).to eq([])
+ end
+
+ it 'gracefully handles unknown attributes' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return([{ unknown_attribute: 1 }, broadcast_message.attributes].to_json)
+
+ expect(cache.read(key, BroadcastMessage)).to eq([broadcast_message])
+ end
+ end
+ end
+
+ describe '#write' do
+ it 'writes value to the cache with the given key' do
+ cache.write(key, true)
+
+ expect(backend).to have_received(:write).with(expanded_key, "true", nil)
+ end
+
+ it 'writes a string containing a JSON representation of the value to the cache' do
+ cache.write(key, broadcast_message)
+
+ expect(backend).to have_received(:write)
+ .with(expanded_key, broadcast_message.to_json, nil)
+ end
+
+ it 'passes options the underlying cache implementation' do
+ cache.write(key, true, expires_in: 15.seconds)
+
+ expect(backend).to have_received(:write)
+ .with(expanded_key, "true", expires_in: 15.seconds)
+ end
+
+ it 'passes options the underlying cache implementation when options is empty' do
+ cache.write(key, true, {})
+
+ expect(backend).to have_received(:write)
+ .with(expanded_key, "true", {})
+ end
+
+ it 'passes options the underlying cache implementation when options is nil' do
+ cache.write(key, true, nil)
+
+ expect(backend).to have_received(:write)
+ .with(expanded_key, "true", nil)
+ end
+ end
+
+ describe '#fetch', :use_clean_rails_memory_store_caching do
+ let(:backend) { Rails.cache }
+
+ it 'requires a block' do
+ expect { cache.fetch(key) }.to raise_error(LocalJumpError)
+ end
+
+ it 'passes options the underlying cache implementation' do
+ expect(backend).to receive(:write)
+ .with(expanded_key, "true", expires_in: 15.seconds)
+
+ cache.fetch(key, expires_in: 15.seconds) { true }
+ end
+
+ context 'when the given key does not exist in the cache' do
+ context 'when the result of the block is truthy' do
+ it 'returns the result of the block' do
+ result = cache.fetch(key) { true }
+
+ expect(result).to eq(true)
+ end
+
+ it 'caches the value' do
+ expect(backend).to receive(:write).with(expanded_key, "true", {})
+
+ cache.fetch(key) { true }
+ end
+ end
+
+ context 'when the result of the block is false' do
+ it 'returns the result of the block' do
+ result = cache.fetch(key) { false }
+
+ expect(result).to eq(false)
+ end
+
+ it 'caches the value' do
+ expect(backend).to receive(:write).with(expanded_key, "false", {})
+
+ cache.fetch(key) { false }
+ end
+ end
+
+ context 'when the result of the block is nil' do
+ it 'returns the result of the block' do
+ result = cache.fetch(key) { nil }
+
+ expect(result).to eq(nil)
+ end
+
+ it 'caches the value' do
+ expect(backend).to receive(:write).with(expanded_key, "null", {})
+
+ cache.fetch(key) { nil }
+ end
+ end
+ end
+
+ context 'when the given key exists in the cache' do
+ context 'when the cached value is a hash' do
+ before do
+ backend.write(expanded_key, broadcast_message.to_json)
+ end
+
+ it 'parses the cached value' do
+ result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+
+ expect(result).to eq(broadcast_message)
+ end
+
+ it "returns the result of the block when 'as' option is nil" do
+ result = cache.fetch(key, as: nil) { 'block result' }
+
+ expect(result).to eq('block result')
+ end
+
+ it "returns the result of the block when 'as' option is not informed" do
+ result = cache.fetch(key) { 'block result' }
+
+ expect(result).to eq('block result')
+ end
+ end
+
+ context 'when the cached value is a array' do
+ before do
+ backend.write(expanded_key, [broadcast_message].to_json)
+ end
+
+ it 'parses the cached value' do
+ result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+
+ expect(result).to eq([broadcast_message])
+ end
+
+ it "returns an empty array when 'as' option is nil" do
+ result = cache.fetch(key, as: nil) { 'block result' }
+
+ expect(result).to eq([])
+ end
+
+ it "returns an empty array when 'as' option is not informed" do
+ result = cache.fetch(key) { 'block result' }
+
+ expect(result).to eq([])
+ end
+ end
+
+ context 'when the cached value is true' do
+ before do
+ backend.write(expanded_key, "true")
+ end
+
+ it 'returns the cached value' do
+ result = cache.fetch(key) { 'block result' }
+
+ expect(result).to eq(true)
+ end
+
+ it 'does not execute the block' do
+ expect { |block| cache.fetch(key, &block) }.not_to yield_control
+ end
+
+ it 'does not write to the cache' do
+ expect(backend).not_to receive(:write)
+
+ cache.fetch(key) { 'block result' }
+ end
+ end
+
+ context 'when the cached value is false' do
+ before do
+ backend.write(expanded_key, "false")
+ end
+
+ it 'returns the cached value' do
+ result = cache.fetch(key) { 'block result' }
+
+ expect(result).to eq(false)
+ end
+
+ it 'does not execute the block' do
+ expect { |block| cache.fetch(key, &block) }.not_to yield_control
+ end
+
+ it 'does not write to the cache' do
+ expect(backend).not_to receive(:write)
+
+ cache.fetch(key) { 'block result' }
+ end
+ end
+
+ context 'when the cached value is nil' do
+ before do
+ backend.write(expanded_key, "null")
+ end
+
+ it 'returns the result of the block' do
+ result = cache.fetch(key) { 'block result' }
+
+ expect(result).to eq('block result')
+ end
+
+ it 'writes the result of the block to the cache' do
+ expect(backend).to receive(:write)
+ .with(expanded_key, 'block result'.to_json, {})
+
+ cache.fetch(key) { 'block result' }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb
index 78974cadb69..78c74266c61 100644
--- a/spec/lib/gitlab/prometheus/query_variables_spec.rb
+++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe Gitlab::Prometheus::QueryVariables do
describe '.call' do
- set(:environment) { create(:environment) }
+ let(:environment) { create(:environment) }
let(:slug) { environment.slug }
subject { described_class.call(environment) }
@@ -20,7 +20,7 @@ describe Gitlab::Prometheus::QueryVariables do
it { is_expected.to include(kube_namespace: '') }
end
- context 'with deplyoment platform' do
+ context 'with deployment platform' do
let(:kube_namespace) { environment.deployment_platform.actual_namespace }
before do
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index d6e5b557870..89839709131 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -49,7 +49,7 @@ describe BroadcastMessage do
it 'caches the output of the query' do
create(:broadcast_message)
- expect(described_class).to receive(:where).and_call_original.once
+ expect(described_class).to receive(:current_and_future_messages).and_call_original.once
described_class.current
@@ -93,27 +93,6 @@ describe BroadcastMessage do
expect(Rails.cache).to receive(:delete).with(described_class::LEGACY_CACHE_KEY)
expect(described_class.current.length).to eq(0)
end
-
- it 'gracefully handles bad cache entry' do
- allow(described_class).to receive(:current_and_future_messages).and_return('{')
-
- expect(described_class.current).to be_empty
- end
-
- it 'gracefully handles an empty hash' do
- allow(described_class).to receive(:current_and_future_messages).and_return('{}')
-
- expect(described_class.current).to be_empty
- end
-
- it 'gracefully handles unknown attributes' do
- message = create(:broadcast_message)
-
- allow(described_class).to receive(:current_and_future_messages)
- .and_return([{ bad_attr: 1 }, message])
-
- expect(described_class.current).to eq([message])
- end
end
describe '#active?' do
diff --git a/spec/models/concerns/avatarable_spec.rb b/spec/models/concerns/avatarable_spec.rb
index 7d617cb7b19..1ea7f2b9985 100644
--- a/spec/models/concerns/avatarable_spec.rb
+++ b/spec/models/concerns/avatarable_spec.rb
@@ -33,6 +33,43 @@ describe Avatarable do
end
describe '#avatar_path' do
+ context 'with caching enabled', :request_store do
+ let!(:avatar_path) { [relative_url_root, project.avatar.local_url].join }
+ let!(:avatar_url) { [gitlab_host, relative_url_root, project.avatar.local_url].join }
+
+ it 'only calls local_url once' do
+ expect(project.avatar).to receive(:local_url).once.and_call_original
+
+ 2.times do
+ expect(project.avatar_path).to eq(avatar_path)
+ end
+ end
+
+ it 'calls local_url twice for path and URLs' do
+ expect(project.avatar).to receive(:local_url).exactly(2).times.and_call_original
+
+ expect(project.avatar_path(only_path: true)).to eq(avatar_path)
+ expect(project.avatar_path(only_path: false)).to eq(avatar_url)
+ end
+
+ it 'calls local_url twice for different sizes' do
+ expect(project.avatar).to receive(:local_url).exactly(2).times.and_call_original
+
+ expect(project.avatar_path).to eq(avatar_path)
+ expect(project.avatar_path(size: 40)).to eq(avatar_path + "?width=40")
+ end
+
+ it 'handles unpersisted objects' do
+ new_project = build(:project, :with_avatar)
+ path = [relative_url_root, new_project.avatar.local_url].join
+ expect(new_project.avatar).to receive(:local_url).exactly(2).times.and_call_original
+
+ 2.times do
+ expect(new_project.avatar_path).to eq(path)
+ end
+ end
+ end
+
using RSpec::Parameterized::TableSyntax
where(:has_asset_host, :visibility_level, :only_path, :avatar_path_prefix) do
diff --git a/spec/models/pool_repository_spec.rb b/spec/models/pool_repository_spec.rb
index 3d3878b8c39..112d4ab56fc 100644
--- a/spec/models/pool_repository_spec.rb
+++ b/spec/models/pool_repository_spec.rb
@@ -23,4 +23,25 @@ describe PoolRepository do
expect(pool.disk_path).to match(%r{\A@pools/\h{2}/\h{2}/\h{64}})
end
end
+
+ describe '#unlink_repository' do
+ let(:pool) { create(:pool_repository, :ready) }
+
+ context 'when the last member leaves' do
+ it 'schedules pool removal' do
+ expect(::ObjectPool::DestroyWorker).to receive(:perform_async).with(pool.id).and_call_original
+
+ pool.unlink_repository(pool.source_project.repository)
+ end
+ end
+
+ context 'when the second member leaves' do
+ it 'does not schedule pool removal' do
+ create(:project, :repository, pool_repository: pool)
+ expect(::ObjectPool::DestroyWorker).not_to receive(:perform_async).with(pool.id)
+
+ pool.unlink_repository(pool.source_project.repository)
+ end
+ end
+ end
end
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index b7324a26ed2..791b64dc356 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -40,4 +40,34 @@ describe EnvironmentEntity do
expect(subject).to include(:metrics_path)
end
end
+
+ context 'with deployment platform' do
+ let(:project) { create(:project, :repository) }
+ let(:environment) { create(:environment, project: project) }
+
+ context 'when deployment platform is a cluster' do
+ before do
+ create(:cluster,
+ :provided_by_gcp,
+ :project,
+ environment_scope: '*',
+ projects: [project])
+ end
+
+ it 'should include cluster_type' do
+ expect(subject).to include(:cluster_type)
+ expect(subject[:cluster_type]).to eq('project_type')
+ end
+ end
+
+ context 'when deployment platform is a Kubernetes Service' do
+ before do
+ create(:kubernetes_service, project: project)
+ end
+
+ it 'should not include cluster_type' do
+ expect(subject).not_to include(:cluster_type)
+ end
+ end
+ end
end
diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb
index 233076ad6fa..be902d7c679 100644
--- a/spec/tasks/gitlab/storage_rake_spec.rb
+++ b/spec/tasks/gitlab/storage_rake_spec.rb
@@ -46,6 +46,16 @@ describe 'rake gitlab:storage:*' do
describe 'gitlab:storage:migrate_to_hashed' do
let(:task) { 'gitlab:storage:migrate_to_hashed' }
+ context 'read-only database' do
+ it 'does nothing' do
+ expect(Gitlab::Database).to receive(:read_only?).and_return(true)
+
+ expect(Project).not_to receive(:with_unmigrated_storage)
+
+ expect { run_rake_task(task) }.to output(/This task requires database write access. Exiting./).to_stderr
+ end
+ end
+
context '0 legacy projects' do
it 'does nothing' do
expect(StorageMigratorWorker).not_to receive(:perform_async)
@@ -92,7 +102,7 @@ describe 'rake gitlab:storage:*' do
stub_env('ID_FROM', 99999)
stub_env('ID_TO', 99999)
- expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=99999/).to_stdout
+ expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=99999/).to_stderr
end
it 'displays a message when project exists but its already migrated' do
@@ -100,7 +110,7 @@ describe 'rake gitlab:storage:*' do
stub_env('ID_FROM', project.id)
stub_env('ID_TO', project.id)
- expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=#{project.id}/).to_stdout
+ expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=#{project.id}/).to_stderr
end
it 'enqueues migration when project can be found' do
diff --git a/spec/workers/object_pool/destroy_worker_spec.rb b/spec/workers/object_pool/destroy_worker_spec.rb
new file mode 100644
index 00000000000..ef74f0ba87c
--- /dev/null
+++ b/spec/workers/object_pool/destroy_worker_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+describe ObjectPool::DestroyWorker do
+ describe '#perform' do
+ context 'when no pool is in the database' do
+ it "doesn't raise an error" do
+ expect do
+ described_class.new.perform(987654321)
+ end.not_to raise_error
+ end
+ end
+
+ context 'when a pool is present' do
+ let(:pool) { create(:pool_repository, :obsolete) }
+
+ subject { described_class.new }
+
+ it 'requests Gitaly to remove the object pool' do
+ expect(Gitlab::GitalyClient).to receive(:call).with(pool.shard_name, :object_pool_service, :delete_object_pool, Object)
+
+ subject.perform(pool.id)
+ end
+
+ it 'destroys the pool' do
+ subject.perform(pool.id)
+
+ expect(PoolRepository.find_by_id(pool.id)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb
index c2c2a5f9121..5aaff27a6b2 100644
--- a/spec/workers/stuck_merge_jobs_worker_spec.rb
+++ b/spec/workers/stuck_merge_jobs_worker_spec.rb
@@ -38,7 +38,8 @@ describe StuckMergeJobsWorker do
create(:merge_request, :locked, merge_jid: '123')
create(:merge_request, :locked, merge_jid: '456')
- expect(Rails).to receive_message_chain(:logger, :info).with('Updated state of locked merge jobs. JIDs: 123, 456')
+ expect(described_class).to receive_message_chain(:logger, :info)
+ .with('Updated state of locked merge jobs. JIDs: 123, 456')
worker.perform
end
diff --git a/yarn.lock b/yarn.lock
index ae9975831dc..2ff5a59d769 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -641,10 +641,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.43.0.tgz#28dee2122d068cd3b925cd9884d97465ebaca12d"
integrity sha512-wuN3NITmyBV9bOXsFfAjtndFrjTlH6Kf3+6aqT5kHKKLe/B4w7uTU1L9H/cyR0wGD7HbOh584a05eDcuH04fPA==
-"@gitlab/ui@^1.15.0":
- version "1.15.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.15.0.tgz#288e189cb99de354aeb4598f9ac8cced5f47e139"
- integrity sha512-Aiv/WABr8lBVJk0eoanSoO07Lr5Nnvuq82IjDnNzcw9enB1DAKvlstC2r9iiMfg1pVgV/uLdDeRFqH9eI1X4Rg==
+"@gitlab/ui@^1.16.0":
+ version "1.16.2"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.16.2.tgz#fecba9f9198db9fa82e922d3c7fe11dd81e3e9a3"
+ integrity sha512-8m11f9ZQaQZyxmJ6YlgRp0wh94CY95L+Dz5nDFKakwgJA6U5XxQutL1Szip/SCq7u47MXkvQpo5kUQwoAz7K2Q==
dependencies:
babel-standalone "^6.26.0"
bootstrap-vue "^2.0.0-rc.11"
@@ -654,7 +654,7 @@
js-beautify "^1.8.8"
lodash "^4.17.11"
url-search-params-polyfill "^5.0.0"
- vue "^2.5.16"
+ vue "^2.5.21"
vue-loader "^15.4.2"
"@sindresorhus/is@^0.7.0":
@@ -9904,11 +9904,16 @@ vue-virtual-scroll-list@^1.2.5:
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.2.5.tgz#bcbd010f7cdb035eba8958ebf807c6214d9a167a"
integrity sha1-vL0BD3zbA166iVjr+AfGIU2aFno=
-vue@^2.5.16, vue@^2.5.17:
+vue@^2.5.17:
version "2.5.17"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.17.tgz#0f8789ad718be68ca1872629832ed533589c6ada"
integrity sha512-mFbcWoDIJi0w0Za4emyLiW72Jae0yjANHbCVquMKijcavBGypqlF7zHRgMa5k4sesdv7hv2rB4JPdZfR+TPfhQ==
+vue@^2.5.21:
+ version "2.5.21"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.21.tgz#3d33dcd03bb813912ce894a8303ab553699c4a85"
+ integrity sha512-Aejvyyfhn0zjVeLvXd70h4hrE4zZDx1wfZqia6ekkobLmUZ+vNFQer53B4fu0EjWBSiqApxPejzkO1Znt3joxQ==
+
vuex@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"