Commit 66affd99 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents e8627751 81ddb692
......@@ -33,3 +33,4 @@ rules:
vue/no-unused-components: off
vue/no-use-v-if-with-v-for: off
vue/no-v-html: off
vue/use-v-on-exact: off
......@@ -37,7 +37,7 @@ export default function initNewListDropdown() {
});
},
renderRow(label) {
const active = boardsStore.findList('title', label.title);
const active = boardsStore.findListByLabelId(label.id);
const $li = $('<li />');
const $a = $('<a />', {
class: active ? `is-active js-board-list-${active.id}` : '',
......@@ -63,7 +63,7 @@ export default function initNewListDropdown() {
const label = options.selectedObj;
e.preventDefault();
if (!boardsStore.findList('title', label.title)) {
if (!boardsStore.findListByLabelId(label.id)) {
boardsStore.new({
title: label.title,
position: boardsStore.state.lists.length - 2,
......
......@@ -55,12 +55,12 @@ class ListIssue {
}
findLabel(findLabel) {
return this.labels.filter(label => label.title === findLabel.title)[0];
return this.labels.find(label => label.id === findLabel.id);
}
removeLabel(removeLabel) {
if (removeLabel) {
this.labels = this.labels.filter(label => removeLabel.title !== label.title);
this.labels = this.labels.filter(label => removeLabel.id !== label.id);
}
}
......@@ -75,7 +75,7 @@ class ListIssue {
}
findAssignee(findAssignee) {
return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
return this.assignees.find(assignee => assignee.id === findAssignee.id);
}
removeAssignee(removeAssignee) {
......
......@@ -184,6 +184,9 @@ const boardsStore = {
});
return filteredList[0];
},
findListByLabelId(id) {
return this.state.lists.find(list => list.type === 'label' && list.label.id === id);
},
updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`);
},
......
......@@ -42,6 +42,11 @@ export default {
type: Object,
required: true,
},
changesEmptyStateIllustration: {
type: String,
required: false,
default: '',
},
},
data() {
return {
......@@ -63,7 +68,7 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
...mapState('diffs', ['showTreeList', 'isLoading']),
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']),
...mapGetters('diffs', ['isParallelView']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() {
......@@ -79,6 +84,13 @@ export default {
showCompareVersions() {
return this.mergeRequestDiffs && this.mergeRequestDiff;
},
renderDiffFiles() {
return (
this.diffFiles.length > 0 ||
(this.startVersion &&
this.startVersion.version_index === this.mergeRequestDiff.version_index)
);
},
},
watch: {
diffViewType() {
......@@ -191,7 +203,7 @@ export default {
<div v-show="showTreeList" class="diff-tree-list"><tree-list /></div>
<div class="diff-files-holder">
<commit-widget v-if="commit" :commit="commit" />
<template v-if="diffFiles.length > 0">
<template v-if="renderDiffFiles">
<diff-file
v-for="file in diffFiles"
:key="file.newPath"
......@@ -199,7 +211,7 @@ export default {
:can-current-user-fork="canCurrentUserFork"
/>
</template>
<no-changes v-else />
<no-changes v-else :changes-empty-state-illustration="changesEmptyStateIllustration" />
</div>
</div>
</div>
......
<script>
import { mapState } from 'vuex';
import emptyImage from '~/../../views/shared/icons/_mr_widget_empty_state.svg';
import { mapGetters } from 'vuex';
import _ from 'underscore';
import { GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
export default {
data() {
return {
emptyImage,
};
components: {
GlButton,
},
props: {
changesEmptyStateIllustration: {
type: String,
required: true,
},
},
computed: {
...mapState({
sourceBranch: state => state.notes.noteableData.source_branch,
targetBranch: state => state.notes.noteableData.target_branch,
newBlobPath: state => state.notes.noteableData.new_blob_path,
}),
...mapGetters(['getNoteableData']),
emptyStateText() {
return sprintf(
__(
'No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}',
),
{
ref_start: '<span class="ref-name">',
ref_end: '</span>',
source_branch: _.escape(this.getNoteableData.source_branch),
target_branch: _.escape(this.getNoteableData.target_branch),
},
false,
);
},
},
};
</script>
<template>
<div class="row empty-state nothing-here-block">
<div class="col-xs-12">
<div class="svg-content"><span v-html="emptyImage"></span></div>
<div class="row empty-state">
<div class="col-12">
<div class="svg-content svg-250"><img :src="changesEmptyStateIllustration" /></div>
</div>
<div class="col-xs-12">
<div class="col-12">
<div class="text-content text-center">
No changes between <span class="ref-name">{{ sourceBranch }}</span> and
<span class="ref-name">{{ targetBranch }}</span>
<span v-html="emptyStateText"></span>
<div class="text-center">
<a :href="newBlobPath" class="btn btn-success"> {{ __('Create commit') }} </a>
<gl-button :href="getNoteableData.new_blob_path" variant="success">{{
__('Create commit')
}}</gl-button>
</div>
</div>
</div>
......
......@@ -17,6 +17,7 @@ export default function initDiffsApp(store) {
endpoint: dataset.endpoint,
projectPath: dataset.projectPath,
currentUser: JSON.parse(dataset.currentUserData) || {},
changesEmptyStateIllustration: dataset.changesEmptyStateIllustration,
};
},
computed: {
......@@ -31,6 +32,7 @@ export default function initDiffsApp(store) {
currentUser: this.currentUser,
projectPath: this.projectPath,
shouldShow: this.activeTab === 'diffs',
changesEmptyStateIllustration: this.changesEmptyStateIllustration,
},
});
},
......
......@@ -54,67 +54,6 @@ export default class DropdownUtils {
return updatedItem;
}
static mergeDuplicateLabels(dataMap, newLabel) {
const updatedMap = dataMap;
const key = newLabel.title;
const hasKeyProperty = Object.prototype.hasOwnProperty.call(updatedMap, key);
if (!hasKeyProperty) {
updatedMap[key] = newLabel;
} else {
const existing = updatedMap[key];
if (!existing.multipleColors) {
existing.multipleColors = [existing.color];
}
existing.multipleColors.push(newLabel.color);
}
return updatedMap;
}
static duplicateLabelColor(labelColors) {
const colors = labelColors;
const spacing = 100 / colors.length;
// Reduce the colors to 4
colors.length = Math.min(colors.length, 4);
const color = colors
.map((c, i) => {
const percentFirst = Math.floor(spacing * i);
const percentSecond = Math.floor(spacing * (i + 1));
return `${c} ${percentFirst}%, ${c} ${percentSecond}%`;
})
.join(', ');
return `linear-gradient(${color})`;
}
static duplicateLabelPreprocessing(data) {
const results = [];
const dataMap = {};
data.forEach(DropdownUtils.mergeDuplicateLabels.bind(null, dataMap));
Object.keys(dataMap).forEach(key => {
const label = dataMap[key];
if (label.multipleColors) {
label.color = DropdownUtils.duplicateLabelColor(label.multipleColors);
label.text_color = '#000000';
}
results.push(label);
});
results.preprocessed = true;
return results;
}
static filterHint(config, item) {
const { input, allowedKeys } = config;
const updatedItem = item;
......
......@@ -79,11 +79,7 @@ export default class FilteredSearchVisualTokens {
static setTokenStyle(tokenContainer, backgroundColor, textColor) {
const token = tokenContainer;
// Labels with linear gradient should not override default background color
if (backgroundColor.indexOf('linear-gradient') === -1) {
token.style.backgroundColor = backgroundColor;
}
token.style.backgroundColor = backgroundColor;
token.style.color = textColor;
if (textColor === '#FFFFFF') {
......@@ -94,18 +90,6 @@ export default class FilteredSearchVisualTokens {
return token;
}
static preprocessLabel(labelsEndpoint, labels) {
let processed = labels;
if (!labels.preprocessed) {
processed = DropdownUtils.duplicateLabelPreprocessing(labels);
AjaxCache.override(labelsEndpoint, processed);
processed.preprocessed = true;
}
return processed;
}
static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
const { baseEndpoint } = filteredSearchInput.dataset;
......@@ -115,7 +99,6 @@ export default class FilteredSearchVisualTokens {
);
return AjaxCache.retrieve(labelsEndpoint)
.then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint))
.then(labels => {
const matchingLabel = (labels || []).find(
label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue,
......
......@@ -7,7 +7,6 @@ import _ from 'underscore';
import { sprintf, __ } from './locale';
import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
import flash from './flash';
import ModalStore from './boards/stores/modal_store';
......@@ -171,23 +170,7 @@ export default class LabelsSelect {
axios
.get(labelUrl)
.then(res => {
let data = _.chain(res.data)
.groupBy(function(label) {
return label.title;
})
.map(function(label) {
var color;
color = _.map(label, function(dup) {
return dup.color;
});
return {
id: label[0].id,
title: label[0].title,
color: color,
duplicate: color.length > 1,
};
})
.value();
let { data } = res;
if ($dropdown.hasClass('js-extra-options')) {
var extraData = [];
if (showNo) {
......@@ -272,15 +255,9 @@ export default class LabelsSelect {
selectedClass.push('dropdown-clear-active');
}
}
if (label.duplicate) {
color = DropdownUtils.duplicateLabelColor(label.color);
} else {
if (label.color != null) {
[color] = label.color;
}
}
if (color) {
colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
if (label.color) {
colorEl =
"<span class='dropdown-label-box' style='background: " + label.color + "'></span>";
} else {
colorEl = '';
}
......@@ -435,7 +412,7 @@ export default class LabelsSelect {
new ListLabel({
id: label.id,
title: label.title,
color: label.color[0],
color: label.color,
textColor: '#fff',
}),
);
......
......@@ -208,6 +208,8 @@ export default class UserTabs {
loadActivityCalendar() {
const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar');
if (!$calendarWrap.length) return;
const calendarPath = $calendarWrap.data('calendarPath');
AjaxCache.retrieve(calendarPath)
......
......@@ -13,5 +13,7 @@ export default {
</script>
<template>
<div class="circle-icon-container append-right-default"><icon :name="name" /></div>
<div class="circle-icon-container append-right-default align-self-start align-self-lg-center">
<icon :name="name" />
</div>
</template>
......@@ -148,8 +148,8 @@
&.btn-xs {
padding: 2px $gl-btn-padding;
font-size: $gl-btn-small-font-size;
line-height: $gl-btn-small-line-height;
font-size: $gl-btn-xs-font-size;
line-height: $gl-btn-xs-line-height;
}
&.btn-success,
......
......@@ -391,3 +391,17 @@ img.emoji {
.mw-460 { max-width: 460px; }
.ws-initial { white-space: initial; }
.min-height-0 { min-height: 0; }
.gl-pl-0 { padding-left: 0; }
.gl-pl-1 { padding-left: #{0.5 * $grid-size}; }
.gl-pl-2 { padding-left: $grid-size; }
.gl-pl-3 { padding-left: #{2 * $grid-size}; }
.gl-pl-4 { padding-left: #{3 * $grid-size}; }
.gl-pl-5 { padding-left: #{4 * $grid-size}; }
.gl-pr-0 { padding-right: 0; }
.gl-pr-1 { padding-right: #{0.5 * $grid-size}; }
.gl-pr-2 { padding-right: $grid-size; }
.gl-pr-3 { padding-right: #{2 * $grid-size}; }
.gl-pr-4 { padding-right: #{3 * $grid-size}; }
.gl-pr-5 { padding-right: #{4 * $grid-size}; }
......@@ -22,6 +22,10 @@
.container-fluid {
.navbar-toggler {
border-left: 1px solid lighten($border-and-box-shadow, 10%);
svg {
fill: $search-and-nav-links;
}
}
}
......@@ -309,12 +313,14 @@ body {
.navbar-nav {
> li {
> a:hover,
> a:focus {
> a:focus,
> button:hover {
color: $theme-gray-900;
}
&.active > a,
&.active > a:hover {
&.active > a:hover,
&.active > button {
color: $white-light;
}
}
......
......@@ -33,6 +33,7 @@
.close-icon {
display: block;
margin: auto;
}
}
......@@ -168,12 +169,6 @@
color: currentColor;
background-color: transparent;
}
.more-icon,
.close-icon {
fill: $white-light;
margin: auto;
}
}
.navbar-nav {
......
......@@ -378,7 +378,9 @@ $gl-btn-line-height: 16px;
$gl-btn-vert-padding: 8px;
$gl-btn-horz-padding: 12px;
$gl-btn-small-font-size: 13px;
$gl-btn-small-line-height: 13px;
$gl-btn-small-line-height: 18px;
$gl-btn-xs-font-size: 13px;
$gl-btn-xs-line-height: 13px;
/*
* Badges
......
......@@ -98,6 +98,31 @@ module EmailsHelper
"#{string} on #{Gitlab.config.gitlab.host}"
end
def create_list_id_string(project, list_id_max_length = 255)
project_path_as_domain = project.full_path.downcase
.split('/').reverse.join('/')
.gsub(%r{[^a-z0-9\/]}, '-')
.gsub(%r{\/+}, '.')
.gsub(/(\A\.+|\.+\z)/, '')
max_domain_length = list_id_max_length - Gitlab.config.gitlab.host.length - project.id.to_s.length - 2
if max_domain_length < 3
return project.id.to_s + "..." + Gitlab.config.gitlab.host
end
if project_path_as_domain.length > max_domain_length
project_path_as_domain = project_path_as_domain.slice(0, max_domain_length)
last_dot_index = project_path_as_domain[0..-2].rindex(".")
last_dot_index ||= max_domain_length - 2
project_path_as_domain = project_path_as_domain.slice(0, last_dot_index).concat("..")
end
project.id.to_s + "." + project_path_as_domain + "." + Gitlab.config.gitlab.host
end
end
EmailsHelper.prepend(EE::EmailsHelper)
......@@ -5,6 +5,7 @@ class Notify < BaseMailer
include ActionDispatch::Routing::PolymorphicRoutes
include GitlabRoutingHelper
include EmailsHelper
include Emails::Issues
include Emails::MergeRequests
......@@ -196,6 +197,7 @@ class Notify < BaseMailer
headers['X-GitLab-Project'] = @project.name
headers['X-GitLab-Project-Id'] = @project.id
headers['X-GitLab-Project-Path'] = @project.full_path
headers['List-Id'] = "#{@project.full_path} <#{create_list_id_string(@project)}>"
end
def add_unsubscription_headers_and_links
......
......@@ -84,6 +84,10 @@ class PoolRepository < ActiveRecord::Base
source_project.repository.raw)
end
def inspect
"#<#{self.class.name} id:#{id} state:#{state} disk_path:#{disk_path} source_project: #{source_project.full_path}>"
end
private
def correct_disk_path
......
- @no_container = true
- page_title _("Environments")
- add_to_breadcrumbs(_("Pipelines"), project_pipelines_path(@project))
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
......
......@@ -77,7 +77,8 @@
#js-diffs-app.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked?,
endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', request.query_parameters),
current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json,
project_path: project_path(@merge_request.project)} }
project_path: project_path(@merge_request.project),
changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg') } }
.mr-loading-status
= spinner
......
---
title: Disable merging of labels with same names
merge_request: 23265
author:
type: changed
---
title: Ensure that SVG sprite icons are properly rendered in IE11
merge_request:
author:
type: fixed
---
title: Add project identifier as List-Id email Header to ease filtering
merge_request: 22817
author: Olivier Crête
type: added
---
title: Define the default value for only/except policies
merge_request: 23531
merge_request: 23765
author:
type: changed
---
title: Fixed merge request diffs empty states
merge_request:
author:
type: fixed
---
title: Fix calendar events fetching error on private profile page
merge_request: 23718
author: Harry Kiselev
type: other
---
title: Update environments breadcrumb
merge_request: 23751
author: George Tsiolis
type: changed
---
title: Update header navigation theme colors
merge_request: 23734
author: George Tsiolis
type: fixed
---
title: Restore Object Pools when restoring an object pool
merge_request: 23682
author:
type: added
......@@ -19,6 +19,16 @@ Here are some things to keep in mind regarding test performance:
## RSpec
To run rspec tests:
```sh
# run all tests
bundle exec rspec
# run test for path
bundle exec rspec spec/[path]/[to]/[spec].rb
```
### General guidelines
- Use a single, top-level `describe ClassName` block.
......
......@@ -657,6 +657,7 @@ Restoring database tables:
- Loading fixture wikis...[SKIPPING]
Restoring repositories:
- Restoring repository abcd... [DONE]
- Object pool 1 ...
Deleting tmp directories...[DONE]
```
......
......@@ -271,7 +271,7 @@ deployments.
| [Cert Manager](http://docs.cert-manager.io/en/latest/) | 11.6+ | Cert Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up-to-date. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. | [stable/prometheus](https://github.com/helm/charts/tree/master/stable/prometheus) |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) |
| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use [this](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) custom Jupyter image that installs additional useful packages on top of the base Jupyter. You will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix). More information on creating executable runbooks can be found at [Nurtch Documentation](http://docs.nurtch.com/en/latest). **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | [jupyter/jupyterhub](https://jupyterhub.github.io/helm-chart/) |
| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use a [custom Jupyter image](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) that installs additional useful packages on top of the base Jupyter. You will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix). More information on creating executable runbooks can be found in [our Nurtch documentation](runbooks/index.md#nurtch-executable-runbooks). **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | [jupyter/jupyterhub](https://jupyterhub.github.io/helm-chart/) |
| [Knative](https://cloud.google.com/knative) | 11.5+ | Knative provides a platform to create, deploy, and manage serverless workloads from a Kubernetes cluster. It is used in conjunction with, and includes [Istio](https://istio.io) to provide an external IP address for all programs hosted by Knative. You will be prompted to enter a wildcard domain where your applications will be exposed. Configure your DNS server to use the external IP address for that domain. For any application created and installed, they will be accessible as `<program_name>.<kubernetes_namespace>.<domain_name>`. This will require your kubernetes cluster to have [RBAC enabled](#role-based-access-control-rbac). | [knative/knative](https://storage.googleapis.com/triggermesh-charts)
NOTE: **Note:**
......
......@@ -138,6 +138,7 @@ Notification emails include headers that provide extra content about the notific
| X-GitLab-Pipeline-Id | Only in pipeline emails, the ID of the pipeline the notification is for |
| X-GitLab-Reply-Key | A unique token to support reply by email |
| X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc |
| List-Id | The path of the project in a RFC 2919 mailing list identifier useful for email organization, for example, with GMail filters |
#### X-GitLab-NotificationReason
......
......@@ -4,6 +4,7 @@ require 'yaml'
module Backup
class Repository
include Gitlab::ShellAdapter
attr_reader :progress
def initialize(progress)
......@@ -75,7 +76,6 @@ module Backup
def restore
prepare_directories
gitlab_shell = Gitlab::Shell.new
Project.find_each(batch_size: 1000) do |project|
progress.print " * #{project.full_path} ... "
......@@ -118,6 +118,8 @@ module Backup
end
end
end
restore_object_pools
end
protected
......@@ -159,5 +161,17 @@ module Backup
def display_repo_path(project)
project.hashed_storage?(:repository) ? "#{project.full_path} (#{project.disk_path})" : project.full_path
end
def restore_object_pools
PoolRepository.includes(:source_project).find_each do |pool|
progress.puts " - Object pool #{pool.disk_path}..."
pool.source_project ||= pool.member_projects.first.root_of_fork_network
pool.state = 'none'
pool.save
pool.schedule
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents an only/except trigger policy for the job.
#
class ExceptPolicy < Policy
def self.default
end
end
end
end
end
end
......@@ -16,6 +16,13 @@ module Gitlab
dependencies before_script after_script variables
environment coverage retry parallel extends].freeze
DEFAULT_ONLY_POLICY = {
refs: %w(branches tags)
}.freeze
DEFAULT_EXCEPT_POLICY = {
}.freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
validates :config, presence: true
......@@ -65,10 +72,10 @@ module Gitlab
entry :services, Entry::Services,
description: 'Services that will be used to execute this job.'
entry :only, Entry::OnlyPolicy,
entry :only, Entry::Policy,
description: 'Refs policy this job will be executed for.'
entry :except, Entry::ExceptPolicy,
entry :except, Entry::Policy,
description: 'Refs policy this job will be executed for.'
entry :variables, Entry::Variables,
......@@ -154,8 +161,8 @@ module Gitlab
services: services_value,
stage: stage_value,
cache: cache_value,
only: only_value,
except: except_value,
only: DEFAULT_ONLY_POLICY.deep_merge(only_value.to_h),
except: DEFAULT_EXCEPT_POLICY.deep_merge(except_value.to_h),
variables: variables_defined? ? variables_value : nil,
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents an only/except trigger policy for the job.
#
class OnlyPolicy < Policy
def self.default
{ refs: %w[branches tags] }
end
end
end
end
end
end
......@@ -5,9 +5,12 @@ module Gitlab
class Config
module Entry
##
# Base class for OnlyPolicy and ExceptPolicy
# Entry that represents an only/except trigger policy for the job.
#
class Policy < ::Gitlab::Config::Entry::Simplifiable
strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) }
strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) }
class RefsPolicy < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
......@@ -63,16 +66,6 @@ module Gitlab
def self.default
end
##
# Class-level execution won't be inherited by subclasses by default.
# Therefore, we need to explicitly execute that for OnlyPolicy and ExceptPolicy
def self.inherited(klass)
super
klass.strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) }
klass.strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) }
end
end
end
end
......
......@@ -5782,6 +5782,9 @@ msgstr ""
msgid "No changes"
msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
......
......@@ -73,4 +73,59 @@ describe EmailsHelper do
end
end
end
describe '#create_list_id_string' do
using RSpec::Parameterized::TableSyntax
where(:full_path, :list_id_path) do
"01234" | "01234"
"5/0123" | "012.."
"45/012" | "012.."
"012" | "012"
"23/01" | "01.23"
"2/01" | "01.2"
"234/01" | "01.."
"4/2/0" | "0.2.4"
"45/2/0" | "0.2.."
"5/23/0" | "0.."
"0-2/5" | "5.0-2"
"0_2/5" | "5.0-2"
"0.2/5" | "5.0-2"
end
with_them do
it 'ellipcizes different variants' do
project = double("project")
allow(project).to receive(:full_path).and_return(full_path)
allow(project).to receive(:id).and_return(12345)
# Set a max length that gives only 5 chars for the project full path
max_length = "12345..#{Gitlab.config.gitlab.host}".length + 5
list_id = create_list_id_string(project, max_length)
expect(list_id).to eq("12345.#{list_id_path}.#{Gitlab.config.gitlab.host}")
expect(list_id).to satisfy { |s| s.length <= max_length }
end
end
end
describe 'Create realistic List-Id identifier' do
using RSpec::Parameterized::TableSyntax
where(:full_path, :list_id_path) do
"gitlab-org/gitlab-ce" | "gitlab-ce.gitlab-org"
"project-name/subproject_name/my.project" | "my-project.subproject-name.project-name"
end
with_them do
it 'Produces the right List-Id' do
project = double("project")
allow(project).to receive(:full_path).and_return(full_path)
allow(project).to receive(:id).and_return(12345)
list_id = create_list_id_string(project)
expect(list_id).to eq("12345.#{list_id_path}.#{Gitlab.config.gitlab.host}")
expect(list_id).to satisfy { |s| s.length <= 255 }
end
end
end
end
......@@ -65,6 +65,13 @@ describe('Store', () => {
expect(list).toBeDefined();
});
it('finds list by label ID', () => {
boardsStore.addList(listObj);
const list = boardsStore.findListByLabelId(listObj.label.id);
expect(list.id).toBe(listObj.id);
});
it('gets issue when new list added', done => {
boardsStore.addList(listObj);
const list = boardsStore.findList('id', listObj.id);
......
......@@ -55,15 +55,27 @@ describe('Issue model', () => {
expect(issue.labels.length).toBe(2);
});
it('does not add existing label', () => {
it('does not add label if label id exists', () => {
issue.addLabel({
id: 1,
title: 'test 2',
color: 'blue',
description: 'testing',
});
expect(issue.labels.length).toBe(1);
expect(issue.labels[0].color).toBe('red');
});
it('adds other label with same title', () => {
issue.addLabel({
id: 2,
title: 'test',
color: 'blue',
description: 'bugs!',
description: 'other test',
});
expect(issue.labels.length).toBe(1);
expect(issue.labels.length).toBe(2);
});
it('finds label', () => {
......
import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { TEST_HOST } from 'spec/test_constants';
import App from '~/diffs/components/app.vue';
import NoChanges from '~/diffs/components/no_changes.vue';
import DiffFile from '~/diffs/components/diff_file.vue';
import createDiffsStore from '../create_diffs_store';
describe('diffs/components/app', () => {
const oldMrTabs = window.mrTabs;
const Component = Vue.extend(App);
let store;
let vm;
beforeEach(() => {
// setup globals (needed for component to mount :/)
window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
window.mrTabs.expandViewContainer = jasmine.createSpy();
window.location.hash = 'ABC_123';
function createComponent(props = {}, extendStore = () => {}) {
const localVue = createLocalVue();
// setup component
const store = createDiffsStore();
localVue.use(Vuex);
store = createDiffsStore();
store.state.diffs.isLoading = false;
vm = mountComponentWithStore(Component, {
store,
props: {
extendStore(store);
vm = shallowMount(localVue.extend(App), {
localVue,
propsData: {
endpoint: `${TEST_HOST}/diff/endpoint`,
projectPath: 'namespace/project',
currentUser: {},
changesEmptyStateIllustration: '',
...props,
},
store,
});
}
beforeEach(() => {
// setup globals (needed for component to mount :/)
window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
window.mrTabs.expandViewContainer = jasmine.createSpy();
window.location.hash = 'ABC_123';
});
afterEach(() => {
......@@ -35,21 +46,53 @@ describe('diffs/components/app', () => {
window.mrTabs = oldMrTabs;
// reset component
vm.$destroy();
vm.destroy();
});
it('does not show commit info', () => {
expect(vm.$el).not.toContainElement('.blob-commit-info');
createComponent();
expect(vm.contains('.blob-commit-info')).toBe(false);
});
it('sets highlighted row if hash exists in location object', done => {
vm.$props.shouldShow = true;
vm.$nextTick()
.then(() => {
expect(vm.$store.state.diffs.highlightedRow).toBe('ABC_123');
})
.then(done)
.catch(done.fail);
createComponent({
shouldShow: true,
});
// Component uses $nextTick so we wait until that has finished
setTimeout(() => {
expect(store.state.diffs.highlightedRow).toBe('ABC_123');
done();
});
});
describe('empty state', () => {
it('renders empty state when no diff files exist', () => {
createComponent();
expect(vm.contains(NoChanges)).toBe(true);
});
it('does not render empty state when diff files exist', () => {
createComponent({}, () => {
store.state.diffs.diffFiles.push({
id: 1,
});
});
expect(vm.contains(NoChanges)).toBe(false);
expect(vm.findAll(DiffFile).length).toBe(1);
});
it('does not render empty state when versions match', () => {
createComponent({}, () => {
store.state.diffs.startVersion = { version_index: 1 };
store.state.diffs.mergeRequestDiff = { version_index: 1 };
});
expect(vm.contains(NoChanges)).toBe(false);
});
});
});
// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { createStore } from '~/mr_notes/stores';
import NoChanges from '~/diffs/components/no_changes.vue';
describe('Diff no changes empty state', () => {
let vm;
function createComponent(extendStore = () => {}) {
const localVue = createLocalVue();
localVue.use(Vuex);
const store = createStore();
extendStore(store);
vm = shallowMount(localVue.extend(NoChanges), {
localVue,
store,
propsData: {
changesEmptyStateIllustration: '',
},
});
}
afterEach(() => {
vm.destroy();
});
it('prevents XSS', () => {
createComponent(store => {
// eslint-disable-next-line no-param-reassign
store.state.notes.noteableData = {
source_branch: '<script>alert("test");</script>',
target_branch: '<script>alert("test");</script>',
};
});
expect(vm.contains('script')).toBe(false);
});
});
......@@ -211,132 +211,6 @@ describe('Dropdown Utils', () => {
});
});
describe('mergeDuplicateLabels', () => {
const dataMap = {
label: {
title: 'label',
color: '#FFFFFF',
},
};
it('should add label to dataMap if it is not a duplicate', () => {
const newLabel = {
title: 'new-label',
color: '#000000',
};
const updated = DropdownUtils.mergeDuplicateLabels(dataMap, newLabel);
expect(updated[newLabel.title]).toEqual(newLabel);
});
it('should merge colors if label is a duplicate', () => {
const duplicate = {
title: 'label',
color: '#000000',
};
const updated = DropdownUtils.mergeDuplicateLabels(dataMap, duplicate);
expect(updated.label.multipleColors).toEqual([dataMap.label.color, duplicate.color]);
});
});
describe('duplicateLabelColor', () => {
it('should linear-gradient 2 colors', () => {
const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000']);
expect(gradient).toEqual(
'linear-gradient(#FFFFFF 0%, #FFFFFF 50%, #000000 50%, #000000 100%)',
);
});
it('should linear-gradient 3 colors', () => {
const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333']);
expect(gradient).toEqual(
'linear-gradient(#FFFFFF 0%, #FFFFFF 33%, #000000 33%, #000000 66%, #333333 66%, #333333 100%)',
);
});
it('should linear-gradient 4 colors', () => {
const gradient = DropdownUtils.duplicateLabelColor([
'#FFFFFF',
'#000000',
'#333333',
'#DDDDDD',
]);
expect(gradient).toEqual(
'linear-gradient(#FFFFFF 0%, #FFFFFF 25%, #000000 25%, #000000 50%, #333333 50%, #333333 75%, #DDDDDD 75%, #DDDDDD 100%)',
);
});
it('should not linear-gradient more than 4 colors', () => {
const gradient = DropdownUtils.duplicateLabelColor([
'#FFFFFF',
'#000000',
'#333333',
'#DDDDDD',
'#EEEEEE',
]);
expect(gradient.indexOf('#EEEEEE')).toBe(-1);
});
});
describe('duplicateLabelPreprocessing', () => {
it('should set preprocessed to true', () => {
const results = DropdownUtils.duplicateLabelPreprocessing([]);
expect(results.preprocessed).toEqual(true);
});
it('should not mutate existing data if there are no duplicates', () => {
const data = [
{
title: 'label1',
color: '#FFFFFF',
},
{
title: 'label2',
color: '#000000',
},
];
const results = DropdownUtils.duplicateLabelPreprocessing(data);
expect(results.length).toEqual(2);
expect(results[0]).toEqual(data[0]);
expect(results[1]).toEqual(data[1]);
});
describe('duplicate labels', () => {
const data = [
{
title: 'label',
color: '#FFFFFF',
},
{
title: 'label',
color: '#000000',
},
];
const results = DropdownUtils.duplicateLabelPreprocessing(data);
it('should merge duplicate labels', () => {
expect(results.length).toEqual(1);
});
it('should convert multiple colored labels into linear-gradient', () => {
expect(results[0].color).toEqual(DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000']));
});
it('should set multiple colored label text color to black', () => {
expect(results[0].text_color).toEqual('#000000');
});
});
});
describe('setDataValueIfSelected', () => {
beforeEach(() => {
spyOn(FilteredSearchDropdownManager, 'addWordToInput').and.callFake(() => {});
......
......@@ -909,16 +909,6 @@ describe('Filtered Search Visual Tokens', () => {
expect(token.style.backgroundColor).not.toEqual(originalBackgroundColor);
});
it('should not set backgroundColor when it is a linear-gradient', () => {
const token = subject.setTokenStyle(
bugLabelToken,
'linear-gradient(135deg, red, blue)',
'white',
);
expect(token.style.backgroundColor).toEqual(bugLabelToken.style.backgroundColor);
});
it('should set textColor', () => {
const token = subject.setTokenStyle(bugLabelToken, 'white', 'black');
......@@ -935,39 +925,6 @@ describe('Filtered Search Visual Tokens', () => {
});
});
describe('preprocessLabel', () => {
const endpoint = 'endpoint';
it('does not preprocess more than once', () => {
let labels = [];
spyOn(DropdownUtils, 'duplicateLabelPreprocessing').and.callFake(() => []);
labels = FilteredSearchVisualTokens.preprocessLabel(endpoint, labels);
FilteredSearchVisualTokens.preprocessLabel(endpoint, labels);
expect(DropdownUtils.duplicateLabelPreprocessing.calls.count()).toEqual(1);
});
describe('not preprocessed before', () => {
it('returns preprocessed labels', () => {
let labels = [];
expect(labels.preprocessed).not.toEqual(true);
labels = FilteredSearchVisualTokens.preprocessLabel(endpoint, labels);
expect(labels.preprocessed).toEqual(true);
});
it('overrides AjaxCache with preprocessed results', () => {
spyOn(AjaxCache, 'override').and.callFake(() => {});
FilteredSearchVisualTokens.preprocessLabel(endpoint, []);
expect(AjaxCache.override.calls.count()).toEqual(1);
});
});
});
describe('updateLabelTokenColor', () => {
const jsonFixtureName = 'labels/project_labels.json';
const dummyEndpoint = '/dummy/endpoint';
......
......@@ -67,6 +67,19 @@ describe Backup::Repository do
end
end
end
context 'restoring object pools' do
it 'schedules restoring of the pool' do
pool_repository = create(:pool_repository, :failed)
pool_repository.delete_object_pool
subject.restore
pool_repository.reload
expect(pool_repository).not_to be_failed
expect(pool_repository.object_pool.exists?).to be(true)
end
end
end
describe '#prepare_directories', :seed_helper do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::ExceptPolicy do
let(:entry) { described_class.new(config) }
it_behaves_like 'correct only except policy'
describe '.default' do
it 'does not have a default value' do
expect(described_class.default).to be_nil
end
end
end
......@@ -161,7 +161,8 @@ describe Gitlab::Ci::Config::Entry::Global do
variables: { 'VAR' => 'value' },
ignore: false,
after_script: ['make clean'],
only: { refs: %w[branches tags] } },
only: { refs: %w[branches tags] },
except: {} },
spinach: { name: :spinach,
before_script: [],
script: %w[spinach],
......@@ -173,7 +174,8 @@ describe Gitlab::Ci::Config::Entry::Global do
variables: {},
ignore: false,
after_script: ['make clean'],
only: { refs: %w[branches tags] } }
only: { refs: %w[branches tags] },
except: {} }
)
end
end
......
......@@ -299,7 +299,8 @@ describe Gitlab::Ci::Config::Entry::Job do
stage: 'test',
ignore: false,
after_script: %w[cleanup],
only: { refs: %w[branches tags] })
only: { refs: %w[branches tags] },
except: {})
end
end
end
......
......@@ -68,13 +68,15 @@ describe Gitlab::Ci::Config::Entry::Jobs do
commands: 'rspec',
ignore: false,
stage: 'test',
only: { refs: %w[branches tags] } },
only: { refs: %w[branches tags] },
except: {} },
spinach: { name: :spinach,
script: %w[spinach],
commands: 'spinach',
ignore: false,
stage: 'test',
only: { refs: %w[branches tags] } })
only: { refs: %w[branches tags] },
except: {} })
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::OnlyPolicy do
let(:entry) { described_class.new(config) }
it_behaves_like 'correct only except policy'
describe '.default' do
it 'haa a default value' do
expect(described_class.default).to eq( { refs: %w[branches tags] } )
end
end
end
require 'spec_helper'
require 'fast_spec_helper'
require_dependency 'active_model'
describe Gitlab::Ci::Config::Entry::Policy do
let(:entry) { described_class.new(config) }
context 'when using simplified policy' do
describe 'validations' do
context 'when entry config value is valid' do
context 'when config is a branch or tag name' do
let(:config) { %w[master feature/branch] }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#value' do
it 'returns refs hash' do
expect(entry.value).to eq(refs: config)
end
end
end
context 'when config is a regexp' do
let(:config) { ['/^issue-.*$/'] }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when config is a special keyword' do
let(:config) { %w[tags triggers branches] }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
end
context 'when entry value is not valid' do
let(:config) { [1] }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include /policy config should be an array of strings or regexps/
end
end
end
end
end
context 'when using complex policy' do
context 'when specifying refs policy' do
let(:config) { { refs: ['master'] } }
it 'is a correct configuraton' do
expect(entry).to be_valid
expect(entry.value).to eq(refs: %w[master])
end
end
context 'when specifying kubernetes policy' do
let(:config) { { kubernetes: 'active' } }
it 'is a correct configuraton' do
expect(entry).to be_valid
expect(entry.value).to eq(kubernetes: 'active')
end
end
context 'when specifying invalid kubernetes policy' do
let(:config) { { kubernetes: 'something' } }
it 'reports an error about invalid policy' do
expect(entry.errors).to include /unknown value: something/
end
end
context 'when specifying valid variables expressions policy' do
let(:config) { { variables: ['$VAR == null'] } }
it 'is a correct configuraton' do
expect(entry).to be_valid
expect(entry.value).to eq(config)
end
end
context 'when specifying variables expressions in invalid format' do
let(:config) { { variables: '$MY_VAR' } }
it 'reports an error about invalid format' do
expect(entry.errors).to include /should be an array of strings/
end
end
context 'when specifying invalid variables expressions statement' do
let(:config) { { variables: ['$MY_VAR =='] } }
it 'reports an error about invalid statement' do
expect(entry.errors).to include /invalid expression syntax/
end
end
context 'when specifying invalid variables expressions token' do
let(:config) { { variables: ['$MY_VAR == 123'] } }
it 'reports an error about invalid expression' do
expect(entry.errors).to include /invalid expression syntax/
end
end
context 'when using invalid variables expressions regexp' do
let(:config) { { variables: ['$MY_VAR =~ /some ( thing/'] } }
it 'reports an error about invalid expression' do
expect(entry.errors).to include /invalid expression syntax/
end
end
context 'when specifying a valid changes policy' do
let(:config) { { changes: %w[some/* paths/**/*.rb] } }
it 'is a correct configuraton' do
expect(entry).to be_valid
expect(entry.value).to eq(config)
end
end
context 'when changes policy is invalid' do
let(:config) { { changes: [1, 2] } }
it 'returns errors' do
expect(entry.errors).to include /changes should be an array of strings/
end
end
context 'when specifying unknown policy' do
let(:config) { { refs: ['master'], invalid: :something } }
it 'returns error about invalid key' do
expect(entry.errors).to include /unknown keys: invalid/
end
end
context 'when policy is empty' do
let(:config) { {} }
it 'is not a valid configuration' do
expect(entry.errors).to include /can't be blank/
end
end
end
context 'when policy strategy does not match' do
let(:config) { 'string strategy' }
it 'returns information about errors' do
expect(entry.errors)
.to include /has to be either an array of conditions or a hash/
end
end
describe '.default' do
it 'does not have a default value' do
expect(described_class.default).to be_nil
......
......@@ -840,6 +840,37 @@ describe Ci::CreatePipelineService do
end
end
context "when config uses variables for only keyword" do
let(:config) do
{
build: {
stage: 'build',
script: 'echo',
only: {
variables: %w($CI)
}
}
}
end
context 'when merge request is specified' do
let(:merge_request) do
create(:merge_request,
source_project: project,
source_branch: ref_name,
target_project: project,
target_branch: 'master')
end
it 'does not create a merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
.to eq(['No stages / jobs for this pipeline.'])
end
end
end
context "when config has 'except: [tags]'" do
let(:config) do
{
......
shared_context 'gitlab email notification' do
set(:project) { create(:project, :repository) }
set(:project) { create(:project, :repository, name: 'a-known-name') }
set(:recipient) { create(:user, email: 'recipient@example.com') }
let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name }
......@@ -62,9 +62,11 @@ end
shared_examples 'an email with X-GitLab headers containing project details' do
it 'has X-GitLab-Project headers' do
aggregate_failures do
full_path_as_domain = "#{project.name}.#{project.namespace.path}"
is_expected.to have_header('X-GitLab-Project', /#{project.name}/)
is_expected.to have_header('X-GitLab-Project-Id', /#{project.id}/)
is_expected.to have_header('X-GitLab-Project-Path', /#{project.full_path}/)
is_expected.to have_header('List-Id', "#{project.full_path} <#{project.id}.#{full_path_as_domain}.#{Gitlab.config.gitlab.host}>")
end
end
end
......
# frozen_string_literal: true
shared_examples 'correct only except policy' do
context 'when using simplified policy' do
describe 'validations' do
context 'when entry config value is valid' do
context 'when config is a branch or tag name' do
let(:config) { %w[master feature/branch] }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#value' do
it 'returns refs hash' do
expect(entry.value).to eq(refs: config)
end
end
end
context 'when config is a regexp' do
let(:config) { ['/^issue-.*$/'] }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when config is a special keyword' do
let(:config) { %w[tags triggers branches] }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
end
context 'when entry value is not valid' do
let(:config) { [1] }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include /policy config should be an array of strings or regexps/
end
end
end
end
end
context 'when using complex policy' do
context 'when specifying refs policy' do
let(:config) { { refs: ['master'] } }
it 'is a correct configuraton' do
expect(entry).to be_valid
expect(entry.value).to eq(refs: %w[master])
end
end
context 'when specifying kubernetes policy' do
let(:config) { { kubernetes: 'active' } }
it 'is a correct configuraton' do
expect(entry).to be_valid
expect(entry.value).to eq(kubernetes: 'active')
end
end
context 'when specifying invalid kubernetes policy' do
let(:config) { { kubernetes: 'something' } }
it 'reports an error about invalid policy' do
expect(entry.errors).to include /unknown value: something/
end
end
context 'when specifying valid variables expressions policy' do
let(:config) { { variables: ['$VAR == null'] } }
it 'is a correct configuraton' do
expect(entry).to be_valid
expect(entry.value).to eq(config)
end
end
context 'when specifying variables expressions in invalid format' do
let(:config) { { variables: '$MY_VAR' } }
it 'reports an error about invalid format' do
expect(entry.errors).to include /should be an array of strings/
end
end
context 'when specifying invalid variables expressions statement' do
let(:config) { { variables: ['$MY_VAR =='] } }
it 'reports an error about invalid statement' do
expect(entry.errors).to include /invalid expression syntax/
end
end
context 'when specifying invalid variables expressions token' do
let(:config) { { variables: ['$MY_VAR == 123'] } }
it 'reports an error about invalid expression' do
expect(entry.errors).to include /invalid expression syntax/
end
end
context 'when using invalid variables expressions regexp' do
let(:config) { { variables: ['$MY_VAR =~ /some ( thing/'] } }
it 'reports an error about invalid expression' do
expect(entry.errors).to include /invalid expression syntax/
end
end
context 'when specifying a valid changes policy' do
let(:config) { { changes: %w[some/* paths/**/*.rb] } }
it 'is a correct configuraton' do
expect(entry).to be_valid
expect(entry.value).to eq(config)
end
end
context 'when changes policy is invalid' do
let(:config) { { changes: [1, 2] } }
it 'returns errors' do
expect(entry.errors).to include /changes should be an array of strings/
end
end
context 'when specifying unknown policy' do
let(:config) { { refs: ['master'], invalid: :something } }
it 'returns error about invalid key' do
expect(entry.errors).to include /unknown keys: invalid/
end
end
context 'when policy is empty' do
let(:config) { {} }
it 'is not a valid configuration' do
expect(entry.errors).to include /can't be blank/
end
end
end
context 'when policy strategy does not match' do
let(:config) { 'string strategy' }
it 'returns information about errors' do
expect(entry.errors)
.to include /has to be either an array of conditions or a hash/
end
end
end
......@@ -74,6 +74,7 @@ describe 'gitlab:app namespace rake task' do
it 'invokes restoration on match' do
allow(YAML).to receive(:load_file)
.and_return({ gitlab_version: gitlab_version })
expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:db:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:repo:restore']).to receive(:invoke)
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment