Commit c0d54888 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'nt/ce-to-ee-tuesday' into 'master'

CE upstream: Tuesday

Closes gitaly#166 and gitaly#168

See merge request !1565
parents 6f47aa0b 577bca03
......@@ -226,7 +226,8 @@ Style/PredicateName:
Style/PreferredHashMethods:
Enabled: false
# Offense count: 64
# Offense count: 62
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, exploded
......
......@@ -271,7 +271,6 @@ group :development do
gem 'brakeman', '~> 3.6.0', require: false
gem 'letter_opener_web', '~> 1.3.0'
gem 'bullet', '~> 5.5.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
# Better errors handler
......@@ -283,6 +282,7 @@ group :development do
end
group :development, :test do
gem 'bullet', '~> 5.5.0', require: !!ENV['ENABLE_BULLET']
gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'pry-rails', '~> 0.3.4'
......
/* eslint-disable class-methods-use-this */
/* global Flash */
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector';
import LicenseSelector from './template_selectors/license_selector';
export default class FileTemplateMediator {
constructor({ editor, currentAction }) {
this.editor = editor;
this.currentAction = currentAction;
this.initTemplateSelectors();
this.initTemplateTypeSelector();
this.initDomElements();
this.initDropdowns();
this.initPageEvents();
}
initTemplateSelectors() {
// Order dictates template type dropdown item order
this.templateSelectors = [
GitignoreSelector,
BlobCiYamlSelector,
DockerfileSelector,
LicenseSelector,
].map(TemplateSelectorClass => new TemplateSelectorClass({ mediator: this }));
}
initTemplateTypeSelector() {
this.typeSelector = new FileTemplateTypeSelector({
mediator: this,
dropdownData: this.templateSelectors
.map((templateSelector) => {
const cfg = templateSelector.config;
return {
name: cfg.name,
key: cfg.key,
};
}),
});
}
initDomElements() {
const $templatesMenu = $('.template-selectors-menu');
const $undoMenu = $templatesMenu.find('.template-selectors-undo-menu');
const $fileEditor = $('.file-editor');
this.$templatesMenu = $templatesMenu;
this.$undoMenu = $undoMenu;
this.$undoBtn = $undoMenu.find('button');
this.$templateSelectors = $templatesMenu.find('.template-selector-dropdowns-wrap');
this.$filenameInput = $fileEditor.find('.js-file-path-name-input');
this.$fileContent = $fileEditor.find('#file-content');
this.$commitForm = $fileEditor.find('form');
this.$navLinks = $fileEditor.find('.nav-links');
}
initDropdowns() {
if (this.currentAction === 'create') {
this.typeSelector.show();
} else {
this.hideTemplateSelectorMenu();
}
this.displayMatchedTemplateSelector();
}
initPageEvents() {
this.listenForFilenameInput();
this.prepFileContentForSubmit();
this.listenForPreviewMode();
}
listenForFilenameInput() {
this.$filenameInput.on('keyup blur', () => {
this.displayMatchedTemplateSelector();
});
}
prepFileContentForSubmit() {
this.$commitForm.submit(() => {
this.$fileContent.val(this.editor.getValue());
});
}
listenForPreviewMode() {
this.$navLinks.on('click', 'a', (e) => {
const urlPieces = e.target.href.split('#');
const hash = urlPieces[1];
if (hash === 'preview') {
this.hideTemplateSelectorMenu();
} else if (hash === 'editor') {
this.showTemplateSelectorMenu();
}
});
}
selectTemplateType(item, el, e) {
if (e) {
e.preventDefault();
}
this.templateSelectors.forEach((selector) => {
if (selector.config.key === item.key) {
selector.show();
} else {
selector.hide();
}
});
this.typeSelector.setToggleText(item.name);
this.cacheToggleText();
}
selectTemplateFile(selector, query, data) {
selector.renderLoading();
// in case undo menu is already already there
this.destroyUndoMenu();
this.fetchFileTemplate(selector.config.endpoint, query, data)
.then((file) => {
this.showUndoMenu();
this.setEditorContent(file);
this.setFilename(selector.config.name);
selector.renderLoaded();
})
.catch(err => new Flash(`An error occurred while fetching the template: ${err}`));
}
displayMatchedTemplateSelector() {
const currentInput = this.getFilename();
this.templateSelectors.forEach((selector) => {
const match = selector.config.pattern.test(currentInput);
if (match) {
this.typeSelector.show();
this.selectTemplateType(selector.config);
this.showTemplateSelectorMenu();
}
});
}
fetchFileTemplate(apiCall, query, data) {
return new Promise((resolve) => {
const resolveFile = file => resolve(file);
if (!data) {
apiCall(query, resolveFile);
} else {
apiCall(query, data, resolveFile);
}
});
}
setEditorContent(file) {
if (!file && file !== '') return;
const newValue = file.content || file;
this.editor.setValue(newValue, 1);
this.editor.focus();
this.editor.navigateFileStart();
}
findTemplateSelectorByKey(key) {
return this.templateSelectors.find(selector => selector.config.key === key);
}
showUndoMenu() {
this.$undoMenu.removeClass('hidden');
this.$undoBtn.on('click', () => {
this.restoreFromCache();
this.destroyUndoMenu();
});
}
destroyUndoMenu() {
this.cacheFileContents();
this.cacheToggleText();
this.$undoMenu.addClass('hidden');
this.$undoBtn.off('click');
}
hideTemplateSelectorMenu() {
this.$templatesMenu.hide();
}
showTemplateSelectorMenu() {
this.$templatesMenu.show();
}
cacheToggleText() {
this.cachedToggleText = this.getTemplateSelectorToggleText();
}
cacheFileContents() {
this.cachedContent = this.editor.getValue();
this.cachedFilename = this.getFilename();
}
restoreFromCache() {
this.setEditorContent(this.cachedContent);
this.setFilename(this.cachedFilename);
this.setTemplateSelectorToggleText();
}
getTemplateSelectorToggleText() {
return this.$templateSelectors
.find('.js-template-selector-wrap:visible .dropdown-toggle-text')
.text();
}
setTemplateSelectorToggleText() {
return this.$templateSelectors
.find('.js-template-selector-wrap:visible .dropdown-toggle-text')
.text(this.cachedToggleText);
}
getTypeSelectorToggleText() {
return this.typeSelector.getToggleText();
}
getFilename() {
return this.$filenameInput.val();
}
setFilename(name) {
this.$filenameInput.val(name);
}
getSelected() {
return this.templateSelectors.find(selector => selector.selected);
}
}
/* global Api */
export default class FileTemplateSelector {
constructor(mediator) {
this.mediator = mediator;
this.$dropdown = null;
this.$wrapper = null;
}
init() {
const cfg = this.config;
this.$dropdown = $(cfg.dropdown);
this.$wrapper = $(cfg.wrapper);
this.$loadingIcon = this.$wrapper.find('.fa-chevron-down');
this.$dropdownToggleText = this.$wrapper.find('.dropdown-toggle-text');
this.initDropdown();
}
show() {
if (this.$dropdown === null) {
this.init();
}
this.$wrapper.removeClass('hidden');
}
hide() {
if (this.$dropdown !== null) {
this.$wrapper.addClass('hidden');
}
}
getToggleText() {
return this.$dropdownToggleText.text();
}
setToggleText(text) {
this.$dropdownToggleText.text(text);
}
renderLoading() {
this.$loadingIcon
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
}
renderLoaded() {
this.$loadingIcon
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
}
reportSelection(query, el, e, data) {
e.preventDefault();
return this.mediator.selectTemplateFile(this, query, data);
}
}
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobCiYamlSelector extends TemplateSelector {
requestFile(query) {
return Api.gitlabCiYml(query.name, (file, config) => this.setEditorContent(file, config));
}
}
/* global Api */
import BlobCiYamlSelector from './blob_ci_yaml_selector';
export default class BlobCiYamlSelectors {
constructor({ editor, $dropdowns }) {
this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
this.initSelectors(editor);
}
initSelectors(editor) {
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobCiYamlSelector({
editor,
pattern: /(.gitlab-ci.yml)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobDockerfileSelector extends TemplateSelector {
requestFile(query) {
return Api.dockerfileYml(query.name, (file, config) => this.setEditorContent(file, config));
}
}
import BlobDockerfileSelector from './blob_dockerfile_selector';
export default class BlobDockerfileSelectors {
constructor({ editor, $dropdowns }) {
this.editor = editor;
this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
this.initSelectors();
}
initSelectors() {
const editor = this.editor;
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobDockerfileSelector({
editor,
pattern: /(Dockerfile)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobGitignoreSelector extends TemplateSelector {
requestFile(query) {
return Api.gitignoreText(query.name, (file, config) => this.setEditorContent(file, config));
}
}
import BlobGitignoreSelector from './blob_gitignore_selector';
export default class BlobGitignoreSelectors {
constructor({ editor, $dropdowns }) {
this.$dropdowns = $dropdowns || $('.js-gitignore-selector');
this.editor = editor;
this.initSelectors();
}
initSelectors() {
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobGitignoreSelector({
pattern: /(.gitignore)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
dropdown: $dropdown,
editor: this.editor,
});
});
}
}
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobLicenseSelector extends TemplateSelector {
requestFile(query) {
const data = {
project: this.dropdown.data('project'),
fullname: this.dropdown.data('fullname'),
};
return Api.licenseText(query.id, data, (file, config) => this.setEditorContent(file, config));
}
}
/* eslint-disable no-unused-vars, no-param-reassign */
import BlobLicenseSelector from './blob_license_selector';
export default class BlobLicenseSelectors {
constructor({ $dropdowns, editor }) {
this.$dropdowns = $dropdowns || $('.js-license-selector');
this.initSelectors(editor);
}
initSelectors(editor) {
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobLicenseSelector({
editor,
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class BlobCiYamlSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'gitlab-ci-yaml',
name: '.gitlab-ci.yml',
pattern: /(.gitlab-ci.yml)/,
endpoint: Api.gitlabCiYml,
dropdown: '.js-gitlab-ci-yml-selector',
wrapper: '.js-gitlab-ci-yml-selector-wrap',
};
}
initDropdown() {
// maybe move to super class as well
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
text: item => item.name,
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class DockerfileSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'dockerfile',
name: 'Dockerfile',
pattern: /(Dockerfile)/,
endpoint: Api.dockerfileYml,
dropdown: '.js-dockerfile-selector',
wrapper: '.js-dockerfile-selector-wrap',
};
}
initDropdown() {
// maybe move to super class as well
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
text: item => item.name,
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class BlobGitignoreSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'gitignore',
name: '.gitignore',
pattern: /(.gitignore)/,
endpoint: Api.gitignoreText,
dropdown: '.js-gitignore-selector',
wrapper: '.js-gitignore-selector-wrap',
};
}
initDropdown() {
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
text: item => item.name,
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class BlobLicenseSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'license',
name: 'LICENSE',
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
endpoint: Api.licenseText,
dropdown: '.js-license-selector',
wrapper: '.js-license-selector-wrap',
};
}
initDropdown() {
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => {
const data = {
project: this.$dropdown.data('project'),
fullname: this.$dropdown.data('fullname'),
};
this.reportSelection(query.id, el, e, data);
},
text: item => item.name,
});
}
}
import FileTemplateSelector from '../file_template_selector';
export default class FileTemplateTypeSelector extends FileTemplateSelector {
constructor({ mediator, dropdownData }) {
super(mediator);
this.mediator = mediator;
this.config = {
dropdown: '.js-template-type-selector',
wrapper: '.js-template-type-selector-wrap',
dropdownData,
};
}
initDropdown() {
this.$dropdown.glDropdown({
data: this.config.dropdownData,
filterable: false,
selectable: true,
toggleLabel: item => item.name,
clicked: (item, el, e) => this.mediator.selectTemplateType(item, el, e),
text: item => item.name,
});
}
}
......@@ -13,8 +13,9 @@ $(() => {
const urlRoot = editBlobForm.data('relative-url-root');
const assetsPath = editBlobForm.data('assets-prefix');
const blobLanguage = editBlobForm.data('blob-language');
const currentAction = $('.js-file-title').data('current-action');
new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage);
new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction);
new NewCommitForm(editBlobForm);
}
......
/* global ace */
import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors';
import BlobGitignoreSelectors from '../blob/template_selectors/blob_gitignore_selectors';
import BlobCiYamlSelectors from '../blob/template_selectors/blob_ci_yaml_selectors';
import BlobDockerfileSelectors from '../blob/template_selectors/blob_dockerfile_selectors';
import TemplateSelectorMediator from '../blob/file_template_mediator';
export default class EditBlob {
constructor(assetsPath, aceMode) {
constructor(assetsPath, aceMode, currentAction) {
this.configureAceEditor(aceMode, assetsPath);
this.prepFileContentForSubmit();
this.initModePanesAndLinks();
this.initSoftWrap();
this.initFileSelectors();
this.initFileSelectors(currentAction);
}
configureAceEditor(aceMode, assetsPath) {
......@@ -19,6 +15,10 @@ export default class EditBlob {
ace.config.loadModule('ace/ext/searchbox');
this.editor = ace.edit('editor');
// This prevents warnings re: automatic scrolling being logged
this.editor.$blockScrolling = Infinity;
this.editor.focus();
if (aceMode) {
......@@ -26,27 +26,11 @@ export default class EditBlob {
}
}
prepFileContentForSubmit() {
$('form').submit(() => {
$('#file-content').val(this.editor.getValue());
});
}
initFileSelectors() {
this.blobTemplateSelectors = [
new BlobLicenseSelectors({
initFileSelectors(currentAction) {
this.fileTemplateMediator = new TemplateSelectorMediator({
currentAction,
editor: this.editor,
}),
new BlobGitignoreSelectors({
editor: this.editor,
}),
new BlobCiYamlSelectors({
editor: this.editor,
}),
new BlobDockerfileSelectors({
editor: this.editor,
}),
];
});
}
initModePanesAndLinks() {
......
/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */
import TemplateSelector from '../blob/template_selectors/template_selector';
import TemplateSelector from '../blob/template_selector';
((global) => {
class IssuableTemplateSelector extends TemplateSelector {
......
.file-editor {
.nav-links {
border-top: 1px solid $border-color;
border-right: 1px solid $border-color;
border-left: 1px solid $border-color;
border-bottom: none;
border-radius: 2px;
background: $gray-normal;
}
#editor {
border: none;
border-radius: 0;
......@@ -72,11 +81,7 @@
}
.encoding-selector,
.soft-wrap-toggle,
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector {
.soft-wrap-toggle {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
......@@ -103,28 +108,9 @@
}
}
}
.gitignore-selector,
.license-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector {
.dropdown {
line-height: 21px;
}
.dropdown-menu-toggle {
vertical-align: top;
width: 220px;
}
}
.gitlab-ci-yml-selector {
.dropdown-menu-toggle {
width: 250px;
}
}
}
@media(max-width: $screen-xs-max){
.file-editor {
.file-title {
......@@ -149,10 +135,7 @@
margin: 3px 0;
}
.encoding-selector,
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector {
.encoding-selector {
display: block;
margin: 3px 0;
......@@ -163,3 +146,104 @@
}
}
}
.blob-new-page-title,
.blob-edit-page-title {
margin: 19px 0 21px;
vertical-align: top;
display: inline-block;
@media(max-width: $screen-sm-max) {
display: block;
margin: 19px 0 12px;
}
}
.template-selectors-menu {
display: inline-block;
vertical-align: top;
margin: 14px 0 0 16px;
padding: 0 0 0 14px;
border-left: 1px solid $border-color;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 5px 0;
padding: 0;
border-left: none;
}
}
.templates-selectors-label {
display: inline-block;
vertical-align: top;
margin-top: 6px;
line-height: 21px;
@media(max-width: $screen-sm-max) {
display: block;
margin: 5px 0;
}
}
.template-selector-dropdowns-wrap {
display: inline-block;
margin-left: 8px;
vertical-align: top;
margin: 5px 0 0 8px;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 0 0 16px;
}
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector,
.template-type-selector {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
margin-top: -5px;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 5px 0;
}
.dropdown {
line-height: 21px;
}
.dropdown-menu-toggle {
width: 250px;
vertical-align: top;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 5px 0;
}
}
}
}
.template-selectors-undo-menu {
display: inline-block;
margin: 7px 0 0 10px;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 20px 0;
}
button {
margin: -4px 0 0 15px;
}
}
......@@ -15,6 +15,9 @@ module IssuableCollections
# a new order into the collection.
# We cannot use reorder to not mess up the paginated collection.
issuable_ids = issuable_collection.map(&:id)
return {} if issuable_ids.empty?
issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
issuable_merge_requests_count =
......
......@@ -59,7 +59,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
def render_ok
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.git_http_ok(repository, user)
render json: Gitlab::Workhorse.git_http_ok(repository, user, action_name)
end
def render_http_not_allowed
......
......@@ -164,11 +164,6 @@ module Ci
builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end
# For now the only user who participates is the user who triggered
def participants(_current_user = nil)
Array(user)
end
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
......
......@@ -60,16 +60,25 @@ class NotificationSetting < ActiveRecord::Base
def set_events
return if custom?
EMAIL_EVENTS.each do |event|
events[event] = false
end
self.events = {}
end
# Validates store accessors values as boolean
# It is a text field so it does not cast correct boolean values in JSON
def events_to_boolean
EMAIL_EVENTS.each do |event|
events[event] = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(events[event])
bool = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(public_send(event))
events[event] = bool
end
end
# Allow people to receive failed pipeline notifications if they already have
# custom notifications enabled, as these are more like mentions than the other
# custom settings.
def failed_pipeline
bool = super
bool.nil? || bool
end
end
......@@ -22,22 +22,21 @@ class KubernetesService < DeploymentService
with_options presence: true, if: :activated? do
validates :api_url, url: true
validates :token
end
validates :namespace,
allow_blank: true,
length: 1..63,
if: :activated?,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message,
},
length: 1..63
end
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
after_save :clear_reactive_cache!
def initialize_properties
if properties.nil?
self.properties = {}
self.namespace = "#{project.path}-#{project.id}" if project.present?
end
self.properties = {} if properties.nil?
end
def title
......@@ -62,7 +61,7 @@ class KubernetesService < DeploymentService
{ type: 'text',
name: 'namespace',
title: 'Kubernetes namespace',
placeholder: 'Kubernetes namespace' },
placeholder: namespace_placeholder },
{ type: 'text',
name: 'api_url',
title: 'API URL',
......@@ -92,7 +91,7 @@ class KubernetesService < DeploymentService
variables = [
{ key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false },
{ key: 'KUBE_NAMESPACE', value: namespace, public: true }
{ key: 'KUBE_NAMESPACE', value: namespace_variable, public: true }
]
if ca_pem.present?
......@@ -132,8 +131,26 @@ class KubernetesService < DeploymentService
{ pods: read_pods, deployments: read_deployments }
end
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
private
def namespace_placeholder
default_namespace || TEMPLATE_PLACEHOLDER
end
def namespace_variable
if namespace.present?
namespace
else
default_namespace
end
end
def default_namespace
"#{project.path}-#{project.id}" if project.present?
end
def build_kubeclient!(api_path: 'api', api_version: 'v1')
raise "Incomplete settings" unless api_url && namespace && token
......
class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[
commit merge confidentiality status label assignee cross_reference
title time_tracking branch milestone discussion task moved approvals
commit merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved opened closed merged
approvals
].freeze
validates :note, presence: true
......
......@@ -24,6 +24,10 @@ class BaseService
Gitlab::AppLogger.info message
end
def log_error(message)
Gitlab::AppLogger.error message
end
def system_hook_service
SystemHooksService.new
end
......
......@@ -5,8 +5,6 @@ module Ci
def execute(pipeline)
@pipeline = pipeline
ensure_created_builds! # TODO, remove me in 9.0
new_builds =
stage_indexes_of_created_builds.map do |index|
process_stage(index)
......@@ -73,18 +71,5 @@ module Ci
def created_builds
pipeline.builds.created
end
# This method is DEPRECATED and should be removed in 9.0.
#
# We need it to maintain backwards compatibility with previous versions
# when builds were not created within one transaction with the pipeline.
#
def ensure_created_builds!
return if created_builds.any?
Ci::CreatePipelineBuildsService
.new(project, current_user)
.execute(pipeline)
end
end
end
......@@ -12,11 +12,7 @@ class NotificationRecipientService
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
recipients = add_project_watchers(recipients)
end
recipients = add_custom_notifications(recipients, custom_action)
recipients = reject_mention_users(recipients)
......@@ -43,6 +39,28 @@ class NotificationRecipientService
recipients.uniq
end
def build_pipeline_recipients(target, current_user, action:)
return [] unless current_user
custom_action =
case action.to_s
when 'failed'
:failed_pipeline
when 'success'
:success_pipeline
end
notification_setting = notification_setting_for_user_project(current_user, target.project)
return [] if notification_setting.mention? || notification_setting.disabled?
return [] if notification_setting.custom? && !notification_setting.public_send(custom_action)
return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
reject_users_without_access([current_user], target)
end
def build_relabeled_recipients(target, current_user, labels:)
recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
......@@ -290,4 +308,16 @@ class NotificationRecipientService
def build_custom_key(action, object)
"#{action}_#{object.class.model_name.name.underscore}".to_sym
end
def notification_setting_for_user_project(user, project)
project_setting = user.notification_settings_for(project)
return project_setting unless project_setting.global?
group_setting = user.notification_settings_for(project.group)
return group_setting unless group_setting.global?
user.global_notification_setting
end
end
......@@ -295,11 +295,11 @@ class NotificationService
return unless mailer.respond_to?(email_template)
recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients(
recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients(
pipeline,
pipeline.user,
action: pipeline.status,
skip_current_user: false).map(&:notification_email)
).map(&:notification_email)
if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later
......
......@@ -46,6 +46,7 @@ module Projects
end
def error(message, http_status = nil)
log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest?
@status.description = message
@status.drop
......
......@@ -183,7 +183,9 @@ module SystemNoteService
body = status.dup
body << " via #{source.gfm_reference(project)}" if source
create_note(NoteSummary.new(noteable, project, author, body, action: 'status'))
action = status == 'reopened' ? 'opened' : status
create_note(NoteSummary.new(noteable, project, author, body, action: action))
end
# Called when 'merge when pipeline succeeds' is executed
......@@ -273,9 +275,15 @@ module SystemNoteService
#
# Returns the created Note object
def change_issue_confidentiality(issue, project, author)
body = issue.confidential ? 'made the issue confidential' : 'made the issue visible to everyone'
if issue.confidential
body = 'made the issue confidential'
action = 'confidential'
else
body = 'made the issue visible to everyone'
action = 'visible'
end
create_note(NoteSummary.new(issue, project, author, body, action: 'confidentiality'))
create_note(NoteSummary.new(issue, project, author, body, action: action))
end
# Called when a branch in Noteable is changed
......
- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create'
.file-holder.file.append-bottom-default
.js-file-title.file-title.clearfix
.js-file-title.file-title.clearfix{ data: { current_action: action } }
.editor-ref
= icon('code-fork')
= ref
%span.editor-file-name
- if current_action?(:edit) || current_action?(:update)
= text_field_tag 'file_path', (params[:file_path] || @path),
class: 'form-control new-file-path'
class: 'form-control new-file-path js-file-path-name-input'
- if current_action?(:new) || current_action?(:create)
%span.editor-file-name
\/
= text_field_tag 'file_name', params[:file_name], placeholder: "File name",
required: true, class: 'form-control new-file-name'
required: true, class: 'form-control new-file-name js-file-path-name-input'
.pull-right.file-buttons
.license-selector.js-license-selector-wrap.hidden
= dropdown_tag("Choose a License template", options: { toggle_class: 'btn js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.hidden
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
= dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.dockerfile-selector.js-dockerfile-selector-wrap.hidden
= dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
= button_tag class: 'soft-wrap-toggle btn', type: 'button' do
= button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do
%span.no-wrap
= custom_icon('icon_no_wrap')
No wrap
......@@ -31,7 +25,7 @@
= custom_icon('icon_soft_wrap')
Soft wrap
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1'
.file-editor.code
%pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data]
......
.template-selectors-menu
.templates-selectors-label
Template
.template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden
= dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } )
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a License template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
.template-selectors-undo-menu.hidden
%span.text-info Template applied
%button.btn.btn-sm.btn-info Undo
......@@ -11,12 +11,15 @@
Someone edited the file the same time you did. Please check out
= link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs.
.editor-title-row
%h3.page-title.blob-edit-page-title
Edit file
= render 'template_selectors'
.file-editor
%ul.nav-links.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
Edit File
Write
%li
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
......
......@@ -2,10 +2,10 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
= page_specific_javascript_bundle_tag('blob')
%h3.page-title
New File
.editor-title-row
%h3.page-title.blob-new-page-title
New file
= render 'template_selectors'
.file-editor
= form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref
......
......@@ -249,6 +249,8 @@
%ul
%li Be careful. Renaming a project's repository can have unintended side effects.
%li You will need to update your local repositories to point to the new location.
- if @project.deployment_services.any?
%li Your deployment services will be broken, you will need to manually fix the services after renaming.
= f.submit 'Rename project', class: "btn btn-warning"
- if can?(current_user, :change_namespace, @project)
%hr
......
......@@ -25,7 +25,7 @@
.form-group
.checkbox{ class: ("prepend-top-0" if index == 0) }
%label{ for: field_id }
= check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
= check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.public_send(event))
%strong
= notification_event_name(event)
= icon("spinner spin", class: "custom-notification-event-loading")
......@@ -53,6 +53,8 @@ class ProcessCommitWorker
def update_issue_metrics(commit, author)
mentioned_issues = commit.all_references(author).issues
return if mentioned_issues.empty?
Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil).
update_all(first_mentioned_in_commit_at: commit.committed_date)
end
......
---
title: Remove no-new annotation from file_template_mediator.js.
merge_request: !9782
author:
---
title: Remove useless queries with false conditions (e.g 1=0)
merge_request: 10141
author: mhasbini
---
title: Added clarification to the Jira integration documentation.
merge_request: 10066
author: Matthew Bender
---
title: Backport API changes needed to fix sticking in EE
merge_request:
author:
---
title: Drop support for correctly processing legacy pipelines
merge_request: 10266
author:
---
title: Fix project creation failure due to race condition in namespace directory creation
merge_request: 10268
author: Robin Bobbitt
---
title: Log errors during generating of Gitlab Pages to debug log
merge_request: 10335
author: Danilo Bargen
---
title: Only email pipeline creators; only email for successful pipelines with custom
settings
merge_request:
author:
---
title: Remove index for users.current sign in at
merge_request: 10401
author: blackst0ne
---
title: Don't fill in the default kubernetes namespace
merge_request:
author:
......@@ -548,7 +548,7 @@ production: &base
storages: # You must have at least a `default` storage path.
default:
path: /home/git/repositories/
gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket
gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket # TCP connections are supported too (e.g. tcp://host:port)
## Backup settings
backup:
......
......@@ -9,7 +9,7 @@ if Gitlab.config.gitaly.enabled || Rails.env.test?
raise "storage #{name.inspect} is missing a gitaly_address"
end
unless URI(address).scheme == 'unix'
unless URI(address).scheme.in?(%w(tcp unix))
raise "Unsupported Gitaly address: #{address.inspect}"
end
......
if ENV['ENABLE_BULLET']
require 'bullet'
if defined?(Bullet) && ENV['ENABLE_BULLET']
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.raise = Rails.env.test?
end
end
end
......@@ -4,10 +4,10 @@ module Db
class AbuseReport
def self.seed
Gitlab::Seeder.quiet do
(::AbuseReport.default_per_page + 3).times do
(::AbuseReport.default_per_page + 3).times do |i|
reported_user =
::User.create!(
username: FFaker::Internet.user_name,
username: "reported_user_#{i}",
name: FFaker::Name.name,
email: FFaker::Internet.email,
confirmed_at: DateTime.now,
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveIndexForUsersCurrentSignInAt < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
if index_exists? :users, :current_sign_in_at
if Gitlab::Database.postgresql?
execute 'DROP INDEX CONCURRENTLY index_users_on_current_sign_in_at;'
else
remove_index :users, :current_sign_in_at
end
end
end
def down
add_concurrent_index :users, :current_sign_in_at
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170329124448) do
ActiveRecord::Schema.define(version: 20170402231018) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -1453,7 +1453,6 @@ ActiveRecord::Schema.define(version: 20170329124448) do
add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree
add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
add_index "users", ["ghost"], name: "index_users_on_ghost", using: :btree
......
<svg version="1.1" id="mscgen_js-svg-__svg" class="mscgen_js-svg-__svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="976" height="310" viewBox="0 0 976 310"><desc>
# Generated by mscgen_js - https://sverweij.github.io/mscgen_js
msc {
# options
hscale="1.5";
# entities
c [label="Client", textbgcolor="lime"],
rails [label="Rails", textbgcolor="cyan"],
etag [label="EtagCaching", textbgcolor="orange"],
redis [label="Redis", textbgcolor="white"];
# arcs
c =&gt; rails [label="GET /projects/5/pipelines"];
rails =&gt; etag [label="GET /projects/5/pipelines"];
etag =&gt; redis [label="read(key = 'GET &lt;Etag&gt;')"];
redis =&gt; etag [label="cache hit", linecolor="green", textcolor="green"];
|||;
etag =&gt; c [label="304 Not Modified", linecolor="blue", textcolor="blue"];
}</desc><defs><style type="text/css">svg.mscgen_js-svg-__svg{font-family:Helvetica,sans-serif;font-size:12px;font-weight:normal;font-style:normal;text-decoration:none;background-color:white;stroke:black;stroke-width:2;color:black}.mscgen_js-svg-__svg path, .mscgen_js-svg-__svg rect{fill:none;color:black;stroke:black}.mscgen_js-svg-__svg .label-text-background{fill:white;stroke:white;stroke-width:0}.mscgen_js-svg-__svg .bglayer{fill:white;stroke:white;stroke-width:0}.mscgen_js-svg-__svg line{stroke:black}.mscgen_js-svg-__svg .return, .mscgen_js-svg-__svg .comment{stroke-dasharray:5,3}.mscgen_js-svg-__svg .inline_expression_divider{stroke-dasharray:10,5}.mscgen_js-svg-__svg text{color:inherit;stroke:none;text-anchor:middle}.mscgen_js-svg-__svg text.entity-text{text-decoration:underline}.mscgen_js-svg-__svg text.anchor-start{text-anchor:start}.mscgen_js-svg-__svg .arrow-marker{overflow:visible}.mscgen_js-svg-__svg .arrow-style{stroke-width:1}.mscgen_js-svg-__svg .arcrow, .mscgen_js-svg-__svg .arcrowomit, .mscgen_js-svg-__svg .emphasised{stroke-linecap:butt}.mscgen_js-svg-__svg .arcrowomit{stroke-dasharray:2,2;}.mscgen_js-svg-__svg .box, .mscgen_js-svg-__svg .entity{fill:white;stroke-linejoin:round}.mscgen_js-svg-__svg .inherit{stroke:inherit;color:inherit}.mscgen_js-svg-__svg .inherit-fill{fill:inherit}.mscgen_js-svg-__svg .watermark{stroke:black;color:black;fill:black;font-size:48pt;font-weight:bold;opacity:0.14}</style><marker orient="auto" id="mscgen_js-svg-__svgmethod-black" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="black" fill="black"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-black" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="black" fill="black"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-blue" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="blue" fill="blue"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-blue" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="blue" fill="blue"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-green" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="green" fill="green"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-green" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="green" fill="green"></polygon></marker></defs><g id="mscgen_js-svg-__svg__body" transform="translate(53,3) scale(1,1)"><g id="mscgen_js-svg-__svg__background"><rect width="976" height="310" x="-53" y="-3" class="bglayer"></rect></g><g id="mscgen_js-svg-__svg__arcspanlayer"></g><g id="mscgen_js-svg-__svg__lifelinelayer"><line x1="75" y1="38" x2="75" y2="76" class="arcrow"></line><line x1="315" y1="38" x2="315" y2="76" class="arcrow"></line><line x1="555" y1="38" x2="555" y2="76" class="arcrow"></line><line x1="795" y1="38" x2="795" y2="76" class="arcrow"></line><line x1="75" y1="76" x2="75" y2="114" class="arcrow"></line><line x1="315" y1="76" x2="315" y2="114" class="arcrow"></line><line x1="555" y1="76" x2="555" y2="114" class="arcrow"></line><line x1="795" y1="76" x2="795" y2="114" class="arcrow"></line><line x1="75" y1="114" x2="75" y2="152" class="arcrow"></line><line x1="315" y1="114" x2="315" y2="152" class="arcrow"></line><line x1="555" y1="114" x2="555" y2="152" class="arcrow"></line><line x1="795" y1="114" x2="795" y2="152" class="arcrow"></line><line x1="75" y1="152" x2="75" y2="190" class="arcrow"></line><line x1="315" y1="152" x2="315" y2="190" class="arcrow"></line><line x1="555" y1="152" x2="555" y2="190" class="arcrow"></line><line x1="795" y1="152" x2="795" y2="190" class="arcrow"></line><line x1="75" y1="190" x2="75" y2="228" class="arcrow"></line><line x1="315" y1="190" x2="315" y2="228" class="arcrow"></line><line x1="555" y1="190" x2="555" y2="228" class="arcrow"></line><line x1="795" y1="190" x2="795" y2="228" class="arcrow"></line><line x1="75" y1="228" x2="75" y2="266" class="arcrow"></line><line x1="315" y1="228" x2="315" y2="266" class="arcrow"></line><line x1="555" y1="228" x2="555" y2="266" class="arcrow"></line><line x1="795" y1="228" x2="795" y2="266" class="arcrow"></line><line x1="75" y1="266" x2="75" y2="304" class="arcrow"></line><line x1="315" y1="266" x2="315" y2="304" class="arcrow"></line><line x1="555" y1="266" x2="555" y2="304" class="arcrow"></line><line x1="795" y1="266" x2="795" y2="304" class="arcrow"></line></g><g id="mscgen_js-svg-__svg__sequencelayer"><g id="mscgen_js-svg-__svgentities"><g><rect width="150" height="38" class="entity" style="fill:lime;"></rect><g><text x="75" y="22.5" class="entity-text "><tspan>Client</tspan></text></g></g><g><rect width="150" height="38" x="240" class="entity" style="fill:cyan;"></rect><g><text x="315" y="22.5" class="entity-text "><tspan>Rails</tspan></text></g></g><g><rect width="150" height="38" x="480" class="entity" style="fill:orange;"></rect><g><text x="555" y="22.5" class="entity-text "><tspan>EtagCaching</tspan></text></g></g><g><rect width="150" height="38" x="720" class="entity" style="fill:white;"></rect><g><text x="795" y="22.5" class="entity-text "><tspan>Redis</tspan></text></g></g></g><g><line x1="75" y1="95" x2="315" y2="95" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="134.06" height="14" x="127.97" y="79.5" class="label-text-background"></rect><text x="195" y="90.5" class="directional-text method-text "><tspan>GET /projects/5/pipelines</tspan></text></g></g><g><line x1="315" y1="133" x2="555" y2="133" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="134.06" height="14" x="367.97" y="117.5" class="label-text-background"></rect><text x="435" y="128.5" class="directional-text method-text "><tspan>GET /projects/5/pipelines</tspan></text></g></g><g><line x1="555" y1="171" x2="795" y2="171" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="135.64" height="14" x="607.17" y="155.5" class="label-text-background"></rect><text x="675" y="166.5" class="directional-text method-text "><tspan>read(key = 'GET &lt;Etag&gt;')</tspan></text></g></g><g><line x1="795" y1="209" x2="555" y2="209" class="arc directional method" style="stroke:green" marker-end="url(#mscgen_js-svg-__svgmethod-green)"></line><g><rect width="48.02" height="14" x="650.98" y="193.5" class="label-text-background"></rect><text x="675" y="204.5" class="directional-text method-text " style="fill:green;"><tspan>cache hit</tspan></text></g></g><g></g><g><line x1="555" y1="285" x2="75" y2="285" class="arc directional method" style="stroke:blue" marker-end="url(#mscgen_js-svg-__svgmethod-blue)"></line><g><rect width="90.72" height="14" x="269.63" y="269.5" class="label-text-background"></rect><text x="315" y="280.5" class="directional-text method-text " style="fill:blue;"><tspan>304 Not Modified</tspan></text></g></g></g><g id="mscgen_js-svg-__svg__notelayer"></g><g id="mscgen_js-svg-__svg__watermark"></g></g></svg>
\ No newline at end of file
<svg version="1.1" id="mscgen_js-svg-__svg" class="mscgen_js-svg-__svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="976" height="386" viewBox="0 0 976 386"><desc>
# Generated by mscgen_js - https://sverweij.github.io/mscgen_js
msc {
# options
hscale="1.5";
# entities
c [label="Client", textbgcolor="lime"],
rails [label="Rails", textbgcolor="cyan"],
etag [label="EtagCaching", textbgcolor="orange"],
redis [label="Redis", textbgcolor="white"];
# arcs
c =&gt; rails [label="GET /projects/5/pipelines"];
rails =&gt; etag [label="GET /projects/5/pipelines"];
etag =&gt; redis [label="read(key = 'GET &lt;Etag&gt;')"];
redis =&gt; etag [label="cache miss", linecolor="red", textcolor="red"];
|||;
etag =&gt; redis [label="write('&lt;New Etag&gt;')"];
etag =&gt; rails [label="GET /projects/5/pipelines"];
rails =&gt; c [label="JSON response w/ ETag", linecolor="blue", textcolor="blue"];
}
</desc><defs><style type="text/css">svg.mscgen_js-svg-__svg{font-family:Helvetica,sans-serif;font-size:12px;font-weight:normal;font-style:normal;text-decoration:none;background-color:white;stroke:black;stroke-width:2}.mscgen_js-svg-__svg path, .mscgen_js-svg-__svg rect{fill:none}.mscgen_js-svg-__svg .label-text-background{fill:white;stroke:white;stroke-width:0}.mscgen_js-svg-__svg .bglayer{fill:white;stroke:white;stroke-width:0}.mscgen_js-svg-__svg line{}.mscgen_js-svg-__svg .return, .mscgen_js-svg-__svg .comment{stroke-dasharray:5,3}.mscgen_js-svg-__svg .inline_expression_divider{stroke-dasharray:10,5}.mscgen_js-svg-__svg text{color:inherit;stroke:none;text-anchor:middle}.mscgen_js-svg-__svg text.entity-text{text-decoration:underline}.mscgen_js-svg-__svg text.anchor-start{text-anchor:start}.mscgen_js-svg-__svg .arrow-marker{overflow:visible}.mscgen_js-svg-__svg .arrow-style{stroke-width:1}.mscgen_js-svg-__svg .arcrow, .mscgen_js-svg-__svg .arcrowomit, .mscgen_js-svg-__svg .emphasised{stroke-linecap:butt}.mscgen_js-svg-__svg .arcrowomit{stroke-dasharray:2,2}.mscgen_js-svg-__svg .box, .mscgen_js-svg-__svg .entity{fill:white;stroke-linejoin:round}.mscgen_js-svg-__svg .inherit{stroke:inherit;color:inherit}.mscgen_js-svg-__svg .inherit-fill{fill:inherit}.mscgen_js-svg-__svg .watermark{font-size:48pt;font-weight:bold;opacity:0.14}</style><marker orient="auto" id="mscgen_js-svg-__svgmethod-black" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="black" fill="black"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-black" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="black" fill="black"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-blue" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="blue" fill="blue"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-blue" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="blue" fill="blue"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-red" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="red" fill="red"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-red" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="red" fill="red"></polygon></marker></defs><g id="mscgen_js-svg-__svg__body" transform="translate(53,3) scale(1,1)"><g id="mscgen_js-svg-__svg__background"><rect width="976" height="386" x="-53" y="-3" class="bglayer"></rect></g><g id="mscgen_js-svg-__svg__arcspanlayer"></g><g id="mscgen_js-svg-__svg__lifelinelayer"><line x1="75" y1="38" x2="75" y2="76" class="arcrow"></line><line x1="315" y1="38" x2="315" y2="76" class="arcrow"></line><line x1="555" y1="38" x2="555" y2="76" class="arcrow"></line><line x1="795" y1="38" x2="795" y2="76" class="arcrow"></line><line x1="75" y1="76" x2="75" y2="114" class="arcrow"></line><line x1="315" y1="76" x2="315" y2="114" class="arcrow"></line><line x1="555" y1="76" x2="555" y2="114" class="arcrow"></line><line x1="795" y1="76" x2="795" y2="114" class="arcrow"></line><line x1="75" y1="114" x2="75" y2="152" class="arcrow"></line><line x1="315" y1="114" x2="315" y2="152" class="arcrow"></line><line x1="555" y1="114" x2="555" y2="152" class="arcrow"></line><line x1="795" y1="114" x2="795" y2="152" class="arcrow"></line><line x1="75" y1="152" x2="75" y2="190" class="arcrow"></line><line x1="315" y1="152" x2="315" y2="190" class="arcrow"></line><line x1="555" y1="152" x2="555" y2="190" class="arcrow"></line><line x1="795" y1="152" x2="795" y2="190" class="arcrow"></line><line x1="75" y1="190" x2="75" y2="228" class="arcrow"></line><line x1="315" y1="190" x2="315" y2="228" class="arcrow"></line><line x1="555" y1="190" x2="555" y2="228" class="arcrow"></line><line x1="795" y1="190" x2="795" y2="228" class="arcrow"></line><line x1="75" y1="228" x2="75" y2="266" class="arcrow"></line><line x1="315" y1="228" x2="315" y2="266" class="arcrow"></line><line x1="555" y1="228" x2="555" y2="266" class="arcrow"></line><line x1="795" y1="228" x2="795" y2="266" class="arcrow"></line><line x1="75" y1="266" x2="75" y2="304" class="arcrow"></line><line x1="315" y1="266" x2="315" y2="304" class="arcrow"></line><line x1="555" y1="266" x2="555" y2="304" class="arcrow"></line><line x1="795" y1="266" x2="795" y2="304" class="arcrow"></line><line x1="75" y1="304" x2="75" y2="342" class="arcrow"></line><line x1="315" y1="304" x2="315" y2="342" class="arcrow"></line><line x1="555" y1="304" x2="555" y2="342" class="arcrow"></line><line x1="795" y1="304" x2="795" y2="342" class="arcrow"></line><line x1="75" y1="342" x2="75" y2="380" class="arcrow"></line><line x1="315" y1="342" x2="315" y2="380" class="arcrow"></line><line x1="555" y1="342" x2="555" y2="380" class="arcrow"></line><line x1="795" y1="342" x2="795" y2="380" class="arcrow"></line></g><g id="mscgen_js-svg-__svg__sequencelayer"><g id="mscgen_js-svg-__svgentities"><g><rect width="150" height="38" class="entity" style="fill:lime;"></rect><g><text x="75" y="22.5" class="entity-text "><tspan>Client</tspan></text></g></g><g><rect width="150" height="38" x="240" class="entity" style="fill:cyan;"></rect><g><text x="315" y="22.5" class="entity-text "><tspan>Rails</tspan></text></g></g><g><rect width="150" height="38" x="480" class="entity" style="fill:orange;"></rect><g><text x="555" y="22.5" class="entity-text "><tspan>EtagCaching</tspan></text></g></g><g><rect width="150" height="38" x="720" class="entity" style="fill:white;"></rect><g><text x="795" y="22.5" class="entity-text "><tspan>Redis</tspan></text></g></g></g><g><line x1="75" y1="95" x2="315" y2="95" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="134.06" height="14" x="127.97" y="79.5" class="label-text-background"></rect><text x="195" y="90.5" class="directional-text method-text "><tspan>GET /projects/5/pipelines</tspan></text></g></g><g><line x1="315" y1="133" x2="555" y2="133" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="134.06" height="14" x="367.97" y="117.5" class="label-text-background"></rect><text x="435" y="128.5" class="directional-text method-text "><tspan>GET /projects/5/pipelines</tspan></text></g></g><g><line x1="555" y1="171" x2="795" y2="171" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="135.64" height="14" x="607.17" y="155.5" class="label-text-background"></rect><text x="675" y="166.5" class="directional-text method-text "><tspan>read(key = 'GET &lt;Etag&gt;')</tspan></text></g></g><g><line x1="795" y1="209" x2="555" y2="209" class="arc directional method" style="stroke:red" marker-end="url(#mscgen_js-svg-__svgmethod-red)"></line><g><rect width="60.02" height="14" x="644.98" y="193.5" class="label-text-background"></rect><text x="675" y="204.5" class="directional-text method-text " style="fill:red;"><tspan>cache miss</tspan></text></g></g><g></g><g><line x1="555" y1="285" x2="795" y2="285" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="103.94" height="14" x="623.02" y="269.5" class="label-text-background"></rect><text x="675" y="280.5" class="directional-text method-text "><tspan>write('&lt;New Etag&gt;')</tspan></text></g></g><g><line x1="555" y1="323" x2="315" y2="323" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="134.06" height="14" x="367.97" y="307.5" class="label-text-background"></rect><text x="435" y="318.5" class="directional-text method-text "><tspan>GET /projects/5/pipelines</tspan></text></g></g><g><line x1="315" y1="361" x2="75" y2="361" class="arc directional method" style="stroke:blue" marker-end="url(#mscgen_js-svg-__svgmethod-blue)"></line><g><rect width="130.72" height="14" x="129.63" y="345.5" class="label-text-background"></rect><text x="195" y="356.5" class="directional-text method-text " style="fill:blue;"><tspan>JSON response w/ ETag</tspan></text></g></g></g><g id="mscgen_js-svg-__svg__notelayer"></g><g id="mscgen_js-svg-__svg__watermark"></g></g></svg>
\ No newline at end of file
......@@ -22,6 +22,9 @@ Instead you should use polling mechanism with ETag caching in Redis.
## How it works
![Cache miss](img/cache-miss.svg)
![Cache hit](img/cache-hit.svg)
1. Whenever a resource changes we generate a random value and store it in
Redis.
1. When a client makes a request we set the `ETag` response header to the value
......
......@@ -57,7 +57,7 @@ sudo -u git -H bundle clean
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production
sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production
```
### 4. Update gitlab-workhorse to the corresponding version
......
......@@ -157,6 +157,11 @@ the same goal:
where `PROJECT-1` is the issue ID of the JIRA project.
>**Note:**
- Only commits and merges into the project's default branch (usually **master**) will
close an issue in Jira. You can change your projects default branch under
[project settings](img/jira_project_settings.png).
### JIRA issue closing example
Let's consider the following example:
......
......@@ -66,14 +66,13 @@ Below is the table of events users can be notified of:
In all of the below cases, the notification will be sent to:
- Participants:
- the author and assignee of the issue/merge request
- the author of the pipeline
- authors of comments on the issue/merge request
- anyone mentioned by `@username` in the issue/merge request title or description
- anyone mentioned by `@username` in any of the comments on the issue/merge request
...with notification level "Participating" or higher
- Watchers: users with notification level "Watch" (however successful pipeline would be off for watchers)
- Watchers: users with notification level "Watch"
- Subscribers: anyone who manually subscribed to the issue/merge request
- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below
......@@ -91,8 +90,8 @@ In all of the below cases, the notification will be sent to:
| Approved merge request | |
| Unapproved merge request | |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
| Failed pipeline | The above, plus the author of the pipeline |
| Successful pipeline | The above, plus the author of the pipeline |
| Failed pipeline | The author of the pipeline |
| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
In addition, if the title or description of an Issue or Merge Request is
changed, notifications will be sent to any **new** mentions by `@username` as
......
......@@ -24,8 +24,5 @@ Capybara.ignore_hidden_elements = false
Capybara::Screenshot.prune_strategy = :keep_last_run
Spinach.hooks.before_run do
require 'spinach/capybara'
require 'capybara/rails'
TestEnv.eager_load_driver_server
end
......@@ -5,10 +5,6 @@ ENV['RAILS_ENV'] = 'test'
require './config/environment'
require 'rspec/expectations'
require_relative 'capybara'
require_relative 'db_cleaner'
require_relative 'rerun'
if ENV['CI']
require 'knapsack'
Knapsack::Adapters::SpinachAdapter.bind
......
......@@ -50,10 +50,14 @@ module API
forbidden!('Job has been erased!') if job.erased?
end
def authenticate_job!(job)
def authenticate_job!
job = Ci::Build.find_by_id(params[:id])
validate_job!(job) do
forbidden! unless job_token_valid?(job)
end
job
end
def job_token_valid?(job)
......
......@@ -113,8 +113,7 @@ module API
optional :state, type: String, desc: %q(Job's status: success, failed)
end
put '/:id' do
job = Ci::Build.find_by_id(params[:id])
authenticate_job!(job)
job = authenticate_job!
job.update_attributes(trace: params[:trace]) if params[:trace]
......@@ -140,8 +139,7 @@ module API
optional :token, type: String, desc: %q(Job's authentication token)
end
patch '/:id/trace' do
job = Ci::Build.find_by_id(params[:id])
authenticate_job!(job)
job = authenticate_job!
error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
content_range = request.headers['Content-Range']
......@@ -175,8 +173,7 @@ module API
require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers)
job = Ci::Build.find_by_id(params[:id])
authenticate_job!(job)
job = authenticate_job!
forbidden!('Job is not running') unless job.running?
if params[:filesize]
......@@ -212,8 +209,7 @@ module API
not_allowed! unless Gitlab.config.artifacts.enabled
require_gitlab_workhorse!
job = Ci::Build.find_by_id(params[:id])
authenticate_job!(job)
job = authenticate_job!
forbidden!('Job is not running!') unless job.running?
artifacts_upload_path = ArtifactUploader.artifacts_upload_path
......@@ -245,8 +241,7 @@ module API
optional :token, type: String, desc: %q(Job's authentication token)
end
get '/:id/artifacts' do
job = Ci::Build.find_by_id(params[:id])
authenticate_job!(job)
job = authenticate_job!
artifacts_file = job.artifacts_file
unless artifacts_file.file_storage?
......
......@@ -86,8 +86,7 @@ module Ci
# Example Request:
# PATCH /builds/:id/trace.txt
patch ":id/trace.txt" do
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
build = authenticate_build!
error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
content_range = request.headers['Content-Range']
......@@ -117,8 +116,7 @@ module Ci
require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers)
not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
build = authenticate_build!
forbidden!('build is not running') unless build.running?
if params[:filesize]
......@@ -154,8 +152,7 @@ module Ci
post ":id/artifacts" do
require_gitlab_workhorse!
not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
build = authenticate_build!
forbidden!('Build is not running!') unless build.running?
artifacts_upload_path = ArtifactUploader.artifacts_upload_path
......@@ -189,8 +186,7 @@ module Ci
# Example Request:
# GET /builds/:id/artifacts
get ":id/artifacts" do
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
build = authenticate_build!
artifacts_file = build.artifacts_file
unless artifacts_file.file_storage?
......@@ -214,8 +210,7 @@ module Ci
# Example Request:
# DELETE /builds/:id/artifacts
delete ":id/artifacts" do
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
build = authenticate_build!
status(200)
build.erase_artifacts!
......
......@@ -13,10 +13,14 @@ module Ci
forbidden! unless current_runner
end
def authenticate_build!(build)
def authenticate_build!
build = Ci::Build.find_by_id(params[:id])
validate_build!(build) do
forbidden! unless build_token_valid?(build)
end
build
end
def validate_build!(build)
......
......@@ -12,9 +12,11 @@ module Gitlab
end
def self.new_channel(address)
# NOTE: Gitaly currently runs on a Unix socket, so permissions are
address = address.sub(%r{^tcp://}, '') if URI(address).scheme == 'tcp'
# NOTE: When Gitaly runs on a Unix socket, permissions are
# handled using the file system and no additional authentication is
# required (therefore the :this_channel_is_insecure flag)
# TODO: Add authentication support when Gitaly is running on a TCP socket.
GRPC::Core::Channel.new(address, {}, :this_channel_is_insecure)
end
......
......@@ -236,7 +236,10 @@ module Gitlab
# add_namespace("/path/to/storage", "gitlab")
#
def add_namespace(storage, name)
FileUtils.mkdir_p(full_path(storage, name), mode: 0770) unless exists?(storage, name)
path = full_path(storage, name)
FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
rescue Errno::EEXIST => e
Rails.logger.warn("Directory exists as a file: #{e} at: #{path}")
end
# Remove directory from repositories storage
......
......@@ -16,7 +16,7 @@ module Gitlab
SECRET_LENGTH = 32
class << self
def git_http_ok(repository, user)
def git_http_ok(repository, user, action)
repo_path = repository.path_to_repo
params = {
GL_ID: Gitlab::GlId.gl_id(user),
......@@ -26,13 +26,25 @@ module Gitlab
if Gitlab.config.gitaly.enabled
storage = repository.project.repository_storage
address = Gitlab::GitalyClient.get_address(storage)
params[:GitalySocketPath] = URI(address).path
# TODO: use GitalyClient code to assemble the Repository message
params[:Repository] = Gitaly::Repository.new(
path: repo_path,
storage_name: storage,
relative_path: Gitlab::RepoPath.strip_storage_path(repo_path),
).to_h
feature_enabled = case action.to_s
when 'git_receive_pack'
Gitlab::GitalyClient.feature_enabled?(:post_receive_pack)
when 'git_upload_pack'
Gitlab::GitalyClient.feature_enabled?(:post_upload_pack)
when 'info_refs'
true
else
raise "Unsupported action: #{action}"
end
params[:GitalySocketPath] = URI(address).path if feature_enabled
end
params
......
......@@ -48,6 +48,10 @@ FactoryGirl.define do
trait :success do
status :success
end
trait :failed do
status :failed
end
end
end
end
......@@ -42,7 +42,7 @@ describe 'Auto deploy' do
it 'includes OpenShift as an available template', js: true do
click_link 'Set up auto deploy'
click_button 'Choose a GitLab CI Yaml template'
click_button 'Apply a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do
expect(page).to have_content('OpenShift')
......@@ -51,7 +51,7 @@ describe 'Auto deploy' do
it 'creates a merge request using "auto-deploy" branch', js: true do
click_link 'Set up auto deploy'
click_button 'Choose a GitLab CI Yaml template'
click_button 'Apply a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do
click_on 'OpenShift'
end
......
......@@ -6,8 +6,8 @@ describe 'Filter issues', js: true, feature: true do
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
let!(:user) { create(:user) }
let!(:user2) { create(:user) }
let!(:user) { create(:user, username: 'joe') }
let!(:user2) { create(:user, username: 'jane') }
let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
......@@ -70,7 +70,7 @@ describe 'Filter issues', js: true, feature: true do
issue_with_caps_label.labels << caps_sensitive_label
issue_with_everything = create(:issue,
title: "Bug report with everything you thought was possible",
title: "Bug report foo was possible",
project: project,
milestone: milestone,
author: user,
......@@ -687,10 +687,10 @@ describe 'Filter issues', js: true, feature: true do
end
it 'filters issues by searched text, author, more text, assignee and even more text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with")
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} foo")
expect_issues_list_count(1)
expect_filtered_search_input('bug report with')
expect_filtered_search_input('bug report foo')
end
it 'filters issues by searched text, author, assignee and label' do
......@@ -701,10 +701,10 @@ describe 'Filter issues', js: true, feature: true do
end
it 'filters issues by searched text, author, text, assignee, text, label and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything")
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} report label:~#{bug_label.title} foo")
expect_issues_list_count(1)
expect_filtered_search_input('bug report with everything')
expect_filtered_search_input('bug report foo')
end
it 'filters issues by searched text, author, assignee, label and milestone' do
......@@ -715,10 +715,10 @@ describe 'Filter issues', js: true, feature: true do
end
it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you")
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} report label:~#{bug_label.title} milestone:%#{milestone.title} foo")
expect_issues_list_count(1)
expect_filtered_search_input('bug report with everything you')
expect_filtered_search_input('bug report foo')
end
it 'filters issues by searched text, author, assignee, multiple labels and milestone' do
......@@ -729,10 +729,10 @@ describe 'Filter issues', js: true, feature: true do
end
it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought")
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} report label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} foo")
expect_issues_list_count(1)
expect_filtered_search_input('bug report with everything you thought')
expect_filtered_search_input('bug report foo')
end
end
......
......@@ -88,7 +88,7 @@ feature 'New blob creation', feature: true, js: true do
scenario 'shows error message' do
expect(page).to have_content('Your changes could not be committed because a file with the same name already exists')
expect(page).to have_content('New File')
expect(page).to have_content('New file')
expect(page).to have_content('NextFeature')
end
end
......
......@@ -40,7 +40,7 @@ feature 'project owner creates a license file', feature: true, js: true do
scenario 'project master creates a license file from the "Add license" link' do
click_link 'Add License'
expect(page).to have_content('New File')
expect(page).to have_content('New file')
expect(current_path).to eq(
namespace_project_new_blob_path(project.namespace, project, 'master'))
expect(find('#file_name').value).to eq('LICENSE')
......@@ -63,7 +63,7 @@ feature 'project owner creates a license file', feature: true, js: true do
def select_template(template)
page.within('.js-license-selector-wrap') do
click_button 'Choose a License template'
click_button 'Apply a License template'
click_link template
wait_for_ajax
end
......
......@@ -14,7 +14,7 @@ feature 'project owner sees a link to create a license file in empty project', f
visit namespace_project_path(project.namespace, project)
click_link 'Create empty bare repository'
click_on 'LICENSE'
expect(page).to have_content('New File')
expect(page).to have_content('New file')
expect(current_path).to eq(
namespace_project_new_blob_path(project.namespace, project, 'master'))
......@@ -40,7 +40,7 @@ feature 'project owner sees a link to create a license file in empty project', f
def select_template(template)
page.within('.js-license-selector-wrap') do
click_button 'Choose a License template'
click_button 'Apply a License template'
click_link template
wait_for_ajax
end
......
require 'spec_helper'
feature 'Template type dropdown selector', js: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.team << [user, :master]
login_as user
end
context 'editing a non-matching file' do
before do
create_and_edit_file('.random-file.js')
end
scenario 'not displayed' do
check_type_selector_display(false)
end
scenario 'selects every template type correctly' do
fill_in 'file_path', with: '.gitignore'
try_selecting_all_types
end
scenario 'updates toggle value when input matches' do
fill_in 'file_path', with: '.gitignore'
check_type_selector_toggle_text('.gitignore')
end
end
context 'editing a matching file' do
before do
visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, 'LICENSE'))
end
scenario 'displayed' do
check_type_selector_display(true)
end
scenario 'is displayed when input matches' do
check_type_selector_display(true)
end
scenario 'selects every template type correctly' do
try_selecting_all_types
end
context 'user previews changes' do
before do
click_link 'Preview Changes'
end
scenario 'type selector is hidden and shown correctly' do
check_type_selector_display(false)
click_link 'Write'
check_type_selector_display(true)
end
end
end
context 'creating a matching file' do
before do
visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitignore')
end
scenario 'is displayed' do
check_type_selector_display(true)
end
scenario 'toggle is set to the correct value' do
check_type_selector_toggle_text('.gitignore')
end
scenario 'selects every template type correctly' do
try_selecting_all_types
end
end
context 'creating a file' do
before do
visit namespace_project_new_blob_path(project.namespace, project, project.default_branch)
end
scenario 'type selector is shown' do
check_type_selector_display(true)
end
scenario 'toggle is set to the proper value' do
check_type_selector_toggle_text('Choose type')
end
scenario 'selects every template type correctly' do
try_selecting_all_types
end
end
end
def check_type_selector_display(is_visible)
count = is_visible ? 1 : 0
expect(page).to have_css('.js-template-type-selector', count: count)
end
def try_selecting_all_types
try_selecting_template_type('LICENSE', 'Apply a License template')
try_selecting_template_type('Dockerfile', 'Apply a Dockerfile template')
try_selecting_template_type('.gitlab-ci.yml', 'Apply a GitLab CI Yaml template')
try_selecting_template_type('.gitignore', 'Apply a .gitignore template')
end
def try_selecting_template_type(template_type, selector_label)
select_template_type(template_type)
check_template_selector_display(selector_label)
check_type_selector_toggle_text(template_type)
end
def select_template_type(template_type)
find('.js-template-type-selector').click
find('.dropdown-content li', text: template_type).click
end
def check_template_selector_display(content)
expect(page).to have_content(content)
end
def check_type_selector_toggle_text(template_type)
dropdown_toggle_button = find('.template-type-selector .dropdown-toggle-text')
expect(dropdown_toggle_button).to have_content(template_type)
end
def create_and_edit_file(file_name)
visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: file_name)
click_button "Commit Changes"
visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, file_name))
end
require 'spec_helper'
include WaitForAjax
feature 'Template Undo Button', js: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.team << [user, :master]
login_as user
end
context 'editing a matching file and applying a template' do
before do
visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, "LICENSE"))
select_file_template('.js-license-selector', 'Apache License 2.0')
end
scenario 'reverts template application' do
try_template_undo('http://www.apache.org/licenses/', 'Apply a License template')
end
end
context 'creating a non-matching file' do
before do
visit namespace_project_new_blob_path(project.namespace, project, 'master')
select_file_template_type('LICENSE')
select_file_template('.js-license-selector', 'Apache License 2.0')
end
scenario 'reverts template application' do
try_template_undo('http://www.apache.org/licenses/', 'Apply a License template')
end
end
end
def try_template_undo(template_content, toggle_text)
check_undo_button_display
check_content_reverted(template_content)
check_toggle_text_set(toggle_text)
end
def check_toggle_text_set(neutral_toggle_text)
expect(page).to have_content(neutral_toggle_text)
end
def check_undo_button_display
expect(page).to have_content('Template applied')
expect(page).to have_css('.template-selectors-undo-menu .btn-info')
end
def check_content_reverted(template_content)
find('.template-selectors-undo-menu .btn-info').click
expect(page).not_to have_content(template_content)
expect(find('.template-type-selector .dropdown-toggle-text')).to have_content()
end
def select_file_template(template_selector_selector, template_name)
find(template_selector_selector).click
find('.dropdown-content li', text: template_name).click
wait_for_ajax
end
def select_file_template_type(template_type)
find('.js-template-type-selector').click
find('.dropdown-content li', text: template_type).click
end
require 'spec_helper'
describe Gitlab::GitalyClient, lib: true do
describe '.new_channel' do
context 'when passed a UNIX socket address' do
it 'passes the address as-is to GRPC::Core::Channel initializer' do
address = 'unix:/tmp/gitaly.sock'
expect(GRPC::Core::Channel).to receive(:new).with(address, any_args)
described_class.new_channel(address)
end
end
context 'when passed a TCP address' do
it 'strips tcp:// prefix before passing it to GRPC::Core::Channel initializer' do
address = 'localhost:9876'
prefixed_address = "tcp://#{address}"
expect(GRPC::Core::Channel).to receive(:new).with(address, any_args)
described_class.new_channel(prefixed_address)
end
end
end
end
......@@ -180,24 +180,68 @@ describe Gitlab::Workhorse, lib: true do
describe '.git_http_ok' do
let(:user) { create(:user) }
let(:repo_path) { repository.path_to_repo }
let(:action) { 'info_refs' }
subject { described_class.git_http_ok(repository, user) }
subject { described_class.git_http_ok(repository, user, action) }
it { expect(subject).to eq({ GL_ID: "user-#{user.id}", RepoPath: repo_path }) }
it { expect(subject).to include({ GL_ID: "user-#{user.id}", RepoPath: repo_path }) }
context 'when Gitaly is enabled' do
let(:gitaly_params) do
{
GitalySocketPath: URI(Gitlab::GitalyClient.get_address('default')).path,
}
end
before do
allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
end
it 'includes Gitaly params in the returned value' do
gitaly_socket_path = URI(Gitlab::GitalyClient.get_address('default')).path
expect(subject).to include({ GitalySocketPath: gitaly_socket_path })
expect(subject[:Repository]).to include({
it 'includes a Repository param' do
repo_param = { Repository: {
path: repo_path,
storage_name: 'default',
relative_path: project.full_path + '.git',
})
} }
expect(subject).to include(repo_param)
end
{
git_receive_pack: :post_receive_pack,
git_upload_pack: :post_upload_pack
}.each do |action_name, feature_flag|
context "when #{action_name} action is passed" do
let(:action) { action_name }
context 'when action is enabled by feature flag' do
it 'includes Gitaly params in the returned value' do
allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(true)
expect(subject).to include(gitaly_params)
end
end
context 'when action is not enabled by feature flag' do
it 'does not include Gitaly params in the returned value' do
allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(false)
expect(subject).not_to include(gitaly_params)
end
end
end
end
context "when info_refs action is passed" do
let(:action) { 'info_refs' }
it { expect(subject).to include(gitaly_params) }
end
context 'when action passed is not supported by Gitaly' do
let(:action) { 'download' }
it { expect { subject }.to raise_exception('Unsupported action: download') }
end
end
end
......
......@@ -1055,10 +1055,13 @@ describe Ci::Pipeline, models: true do
end
before do
reset_delivered_emails!
project.team << [pipeline.user, Gitlab::Access::DEVELOPER]
pipeline.user.global_notification_setting.
update(level: 'custom', failed_pipeline: true, success_pipeline: true)
reset_delivered_emails!
perform_enqueued_jobs do
pipeline.enqueue
pipeline.run
......
......@@ -4,7 +4,7 @@ describe KubernetesService, models: true, caching: true do
include KubernetesHelpers
include ReactiveCachingHelpers
let(:project) { create(:kubernetes_project) }
let(:project) { build_stubbed(:kubernetes_project) }
let(:service) { project.kubernetes_service }
describe "Associations" do
......@@ -14,7 +14,8 @@ describe KubernetesService, models: true, caching: true do
describe 'Validations' do
context 'when service is active' do
before { subject.active = true }
it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.not_to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:api_url) }
it { is_expected.to validate_presence_of(:token) }
......@@ -37,7 +38,7 @@ describe KubernetesService, models: true, caching: true do
'a.b' => false,
'a*b' => false,
}.each do |namespace, validity|
it "should validate #{namespace} as #{validity ? 'valid' : 'invalid'}" do
it "validates #{namespace} as #{validity ? 'valid' : 'invalid'}" do
subject.namespace = namespace
expect(subject.valid?).to eq(validity)
......@@ -48,24 +49,40 @@ describe KubernetesService, models: true, caching: true do
context 'when service is inactive' do
before { subject.active = false }
it { is_expected.not_to validate_presence_of(:namespace) }
it { is_expected.not_to validate_presence_of(:api_url) }
it { is_expected.not_to validate_presence_of(:token) }
end
end
describe '#initialize_properties' do
context 'with a project' do
let(:namespace_name) { "#{project.path}-#{project.id}" }
context 'without a project' do
it 'leaves the namespace unset' do
expect(described_class.new.namespace).to be_nil
end
end
end
it 'defaults to the project name with ID' do
expect(described_class.new(project: project).namespace).to eq(namespace_name)
describe '#fields' do
let(:kube_namespace) do
subject.fields.find { |h| h[:name] == 'namespace' }
end
context 'as template' do
before { subject.template = true }
it 'sets the namespace to the default' do
expect(kube_namespace).not_to be_nil
expect(kube_namespace[:placeholder]).to eq(subject.class::TEMPLATE_PLACEHOLDER)
end
end
context 'without a project' do
it 'leaves the namespace unset' do
expect(described_class.new.namespace).to be_nil
context 'with associated project' do
before { subject.project = project }
it 'sets the namespace to the default' do
expect(kube_namespace).not_to be_nil
expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
end
end
end
......@@ -123,38 +140,40 @@ describe KubernetesService, models: true, caching: true do
before do
subject.api_url = 'https://kube.domain.com'
subject.token = 'token'
subject.namespace = 'my-project'
subject.ca_pem = 'CA PEM DATA'
subject.project = project
end
it 'sets KUBE_URL' do
expect(subject.predefined_variables).to include(
{ key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }
)
end
context 'namespace is provided' do
before { subject.namespace = 'my-project' }
it 'sets KUBE_TOKEN' do
it 'sets the variables' do
expect(subject.predefined_variables).to include(
{ key: 'KUBE_TOKEN', value: 'token', public: false }
{ key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
{ key: 'KUBE_TOKEN', value: 'token', public: false },
{ key: 'KUBE_NAMESPACE', value: 'my-project', public: true },
{ key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
{ key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true },
)
end
it 'sets KUBE_NAMESPACE' do
expect(subject.predefined_variables).to include(
{ key: 'KUBE_NAMESPACE', value: 'my-project', public: true }
)
end
it 'sets KUBE_CA_PEM' do
context 'no namespace provided' do
it 'sets the variables' do
expect(subject.predefined_variables).to include(
{ key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }
{ key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
{ key: 'KUBE_TOKEN', value: 'token', public: false },
{ key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
{ key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true },
)
end
it 'sets KUBE_CA_PEM_FILE' do
expect(subject.predefined_variables).to include(
{ key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
)
it 'sets the KUBE_NAMESPACE' do
kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
expect(kube_namespace).not_to be_nil
expect(kube_namespace[:value]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
end
end
end
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::Members, api: true do
include ApiHelpers
let(:master) { create(:user) }
let(:master) { create(:user, username: 'master_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::V3::Members, api: true do
include ApiHelpers
let(:master) { create(:user) }
let(:master) { create(:user, username: 'master_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
......
......@@ -418,65 +418,6 @@ describe Ci::ProcessPipelineService, '#execute', :services do
end
end
context 'when there are builds that are not created yet' do
let(:pipeline) do
create(:ci_pipeline, config: config)
end
let(:config) do
{ rspec: { stage: 'test', script: 'rspec' },
deploy: { stage: 'deploy', script: 'rsync' } }
end
before do
create_build('linux', stage: 'build', stage_idx: 0)
create_build('mac', stage: 'build', stage_idx: 0)
end
it 'processes the pipeline' do
# Currently we have five builds with state created
#
expect(builds.count).to eq(0)
expect(all_builds.count).to eq(2)
# Process builds service will enqueue builds from the first stage.
#
process_pipeline
expect(builds.count).to eq(2)
expect(all_builds.count).to eq(2)
# When builds succeed we will enqueue remaining builds.
#
# We will have 2 succeeded, 1 pending (from stage test), total 4 (two
# additional build from `.gitlab-ci.yml`).
#
succeed_pending
process_pipeline
expect(builds.success.count).to eq(2)
expect(builds.pending.count).to eq(1)
expect(all_builds.count).to eq(4)
# When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage.
#
succeed_pending
process_pipeline
expect(builds.pending.count).to eq(1)
expect(builds.success.count).to eq(3)
expect(all_builds.count).to eq(4)
# When the last one succeeds we have 4 successful builds.
#
succeed_pending
process_pipeline
expect(builds.success.count).to eq(4)
expect(all_builds.count).to eq(4)
end
end
def process_pipeline
described_class.new(pipeline.project, user).execute(pipeline)
end
......
This diff is collapsed.
......@@ -221,26 +221,23 @@ describe SystemNoteService, services: true do
describe '.change_status' do
subject { described_class.change_status(noteable, project, author, status, source) }
let(:status) { 'new_status' }
context 'with status reopened' do
let(:status) { 'reopened' }
let(:source) { nil }
it_behaves_like 'a system note' do
let(:action) { 'status' }
let(:action) { 'opened' }
end
end
context 'with a source' do
let(:status) { 'opened' }
let(:source) { double('commit', gfm_reference: 'commit 123456') }
it 'sets the note text' do
expect(subject.note).to eq "#{status} via commit 123456"
end
end
context 'without a source' do
it 'sets the note text' do
expect(subject.note).to eq status
end
end
end
describe '.merge_when_pipeline_succeeds' do
......@@ -298,9 +295,23 @@ describe SystemNoteService, services: true do
describe '.change_issue_confidentiality' do
subject { described_class.change_issue_confidentiality(noteable, project, author) }
context 'when noteable responds to `confidential`' do
context 'issue has been made confidential' do
before do
noteable.update_attribute(:confidential, true)
end
it_behaves_like 'a system note' do
let(:action) { 'confidential' }
end
it 'sets the note text' do
expect(subject.note).to eq 'made the issue confidential'
end
end
context 'issue has been made visible' do
it_behaves_like 'a system note' do
let(:action) { 'confidentiality' }
let(:action) { 'visible' }
end
it 'sets the note text' do
......
......@@ -33,4 +33,19 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
expect(meta_data[id].upvotes).to eq(id + 2)
end
end
describe "when given empty collection" do
let(:project2) { create(:empty_project, :public) }
it "doesn't execute any queries with false conditions" do
get_action =
if action
proc { get action }
else
proc { get :index, namespace_id: project2.namespace, project_id: project2 }
end
expect(&get_action).not_to make_queries_matching(/WHERE (?:1=0|0=1)/)
end
end
end
RSpec::Matchers.define :make_queries_matching do |matcher, expected_count = nil|
supports_block_expectations
match do |block|
@counter = query_count(matcher, &block)
if expected_count
@counter.count == expected_count
else
@counter.count > 0
end
end
failure_message_when_negated do |_|
if expected_count
"expected #{matcher} not to match #{expected_count} queries, got #{@counter.count} matches:\n\n#{@counter.inspect}"
else
"expected #{matcher} not to match any query, got #{@counter.count} matches:\n\n#{@counter.inspect}"
end
end
failure_message do |_|
if expected_count
"expected #{matcher} to match #{expected_count} queries, got #{@counter.count} matches:\n\n#{@counter.inspect}"
else
"expected #{matcher} to match at least one query, got #{@counter.count} matches:\n\n#{@counter.inspect}"
end
end
def query_count(regex, &block)
@recorder = ActiveRecord::QueryRecorder.new(&block).log
@recorder.select{ |q| q.match(regex) }
end
end
......@@ -3,131 +3,19 @@ require 'spec_helper'
describe PipelineNotificationWorker do
include EmailHelpers
let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: project.commit('master').sha,
user: pusher,
status: status)
end
let(:project) { create(:project, :repository, public_builds: false) }
let(:user) { create(:user) }
let(:pusher) { user }
let(:watcher) { pusher }
let(:pipeline) { create(:ci_pipeline) }
describe '#execute' do
before do
reset_delivered_emails!
pipeline.project.team << [pusher, Gitlab::Access::DEVELOPER]
end
it 'calls NotificationService#pipeline_finished when the pipeline exists' do
expect(NotificationService).to receive_message_chain(:new, :pipeline_finished)
context 'when watcher has developer access' do
before do
pipeline.project.team << [watcher, Gitlab::Access::DEVELOPER]
end
shared_examples 'sending emails' do
it 'sends emails' do
perform_enqueued_jobs do
subject.perform(pipeline.id)
end
emails = ActionMailer::Base.deliveries
actual = emails.flat_map(&:bcc).sort
expected_receivers = receivers.map(&:email).uniq.sort
expect(actual).to eq(expected_receivers)
expect(emails.size).to eq(1)
expect(emails.last.subject).to include(email_subject)
end
end
context 'with success pipeline' do
let(:status) { 'success' }
let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
let(:receivers) { [pusher, watcher] }
it_behaves_like 'sending emails'
context 'with pipeline from someone else' do
let(:pusher) { create(:user) }
let(:watcher) { user }
context 'with success pipeline notification on' do
before do
watcher.global_notification_setting.
update(level: 'custom', success_pipeline: true)
end
it_behaves_like 'sending emails'
end
context 'with success pipeline notification off' do
let(:receivers) { [pusher] }
before do
watcher.global_notification_setting.
update(level: 'custom', success_pipeline: false)
end
it_behaves_like 'sending emails'
end
end
context 'with failed pipeline' do
let(:status) { 'failed' }
let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
it_behaves_like 'sending emails'
it 'does nothing when the pipeline does not exist' do
expect(NotificationService).not_to receive(:new)
context 'with pipeline from someone else' do
let(:pusher) { create(:user) }
let(:watcher) { user }
context 'with failed pipeline notification on' do
before do
watcher.global_notification_setting.
update(level: 'custom', failed_pipeline: true)
end
it_behaves_like 'sending emails'
end
context 'with failed pipeline notification off' do
let(:receivers) { [pusher] }
before do
watcher.global_notification_setting.
update(level: 'custom', failed_pipeline: false)
end
it_behaves_like 'sending emails'
end
end
end
end
end
context 'when watcher has no read_build access' do
let(:status) { 'failed' }
let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
let(:watcher) { create(:user) }
before do
pipeline.project.team << [watcher, Gitlab::Access::GUEST]
watcher.global_notification_setting.
update(level: 'custom', failed_pipeline: true)
perform_enqueued_jobs do
subject.perform(pipeline.id)
end
end
it 'does not send emails' do
should_only_email(pusher, kind: :bcc)
end
subject.perform(Ci::Pipeline.maximum(:id).to_i.succ)
end
end
end
......@@ -99,6 +99,13 @@ describe ProcessCommitWorker do
expect(metric.first_mentioned_in_commit_at).to eq(commit.committed_date)
end
it "doesn't execute any queries with false conditions" do
allow(commit).to receive(:safe_message).
and_return("Lorem Ipsum")
expect { worker.update_issue_metrics(commit, user) }.not_to make_queries_matching(/WHERE (?:1=0|0=1)/)
end
end
describe '#build_commit' do
......
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