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: ...@@ -226,7 +226,8 @@ Style/PredicateName:
Style/PreferredHashMethods: Style/PreferredHashMethods:
Enabled: false Enabled: false
# Offense count: 64
# Offense count: 62
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, exploded # SupportedStyles: compact, exploded
......
...@@ -271,7 +271,6 @@ group :development do ...@@ -271,7 +271,6 @@ group :development do
gem 'brakeman', '~> 3.6.0', require: false gem 'brakeman', '~> 3.6.0', require: false
gem 'letter_opener_web', '~> 1.3.0' gem 'letter_opener_web', '~> 1.3.0'
gem 'bullet', '~> 5.5.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
# Better errors handler # Better errors handler
...@@ -283,6 +282,7 @@ group :development do ...@@ -283,6 +282,7 @@ group :development do
end end
group :development, :test do group :development, :test do
gem 'bullet', '~> 5.5.0', require: !!ENV['ENABLE_BULLET']
gem 'pry-byebug', '~> 3.4.1', platform: :mri gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'pry-rails', '~> 0.3.4' 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 @@ $(() => { ...@@ -13,8 +13,9 @@ $(() => {
const urlRoot = editBlobForm.data('relative-url-root'); const urlRoot = editBlobForm.data('relative-url-root');
const assetsPath = editBlobForm.data('assets-prefix'); const assetsPath = editBlobForm.data('assets-prefix');
const blobLanguage = editBlobForm.data('blob-language'); 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); new NewCommitForm(editBlobForm);
} }
......
/* global ace */ /* global ace */
import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors'; import TemplateSelectorMediator from '../blob/file_template_mediator';
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';
export default class EditBlob { export default class EditBlob {
constructor(assetsPath, aceMode) { constructor(assetsPath, aceMode, currentAction) {
this.configureAceEditor(aceMode, assetsPath); this.configureAceEditor(aceMode, assetsPath);
this.prepFileContentForSubmit();
this.initModePanesAndLinks(); this.initModePanesAndLinks();
this.initSoftWrap(); this.initSoftWrap();
this.initFileSelectors(); this.initFileSelectors(currentAction);
} }
configureAceEditor(aceMode, assetsPath) { configureAceEditor(aceMode, assetsPath) {
...@@ -19,6 +15,10 @@ export default class EditBlob { ...@@ -19,6 +15,10 @@ export default class EditBlob {
ace.config.loadModule('ace/ext/searchbox'); ace.config.loadModule('ace/ext/searchbox');
this.editor = ace.edit('editor'); this.editor = ace.edit('editor');
// This prevents warnings re: automatic scrolling being logged
this.editor.$blockScrolling = Infinity;
this.editor.focus(); this.editor.focus();
if (aceMode) { if (aceMode) {
...@@ -26,27 +26,11 @@ export default class EditBlob { ...@@ -26,27 +26,11 @@ export default class EditBlob {
} }
} }
prepFileContentForSubmit() { initFileSelectors(currentAction) {
$('form').submit(() => { this.fileTemplateMediator = new TemplateSelectorMediator({
$('#file-content').val(this.editor.getValue()); currentAction,
});
}
initFileSelectors() {
this.blobTemplateSelectors = [
new BlobLicenseSelectors({
editor: this.editor, editor: this.editor,
}), });
new BlobGitignoreSelectors({
editor: this.editor,
}),
new BlobCiYamlSelectors({
editor: this.editor,
}),
new BlobDockerfileSelectors({
editor: this.editor,
}),
];
} }
initModePanesAndLinks() { initModePanesAndLinks() {
......
/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */ /* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */ /* global Api */
import TemplateSelector from '../blob/template_selectors/template_selector'; import TemplateSelector from '../blob/template_selector';
((global) => { ((global) => {
class IssuableTemplateSelector extends TemplateSelector { class IssuableTemplateSelector extends TemplateSelector {
......
.file-editor { .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 { #editor {
border: none; border: none;
border-radius: 0; border-radius: 0;
...@@ -72,11 +81,7 @@ ...@@ -72,11 +81,7 @@
} }
.encoding-selector, .encoding-selector,
.soft-wrap-toggle, .soft-wrap-toggle {
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
font-family: $regular_font; font-family: $regular_font;
...@@ -103,28 +108,9 @@ ...@@ -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){ @media(max-width: $screen-xs-max){
.file-editor { .file-editor {
.file-title { .file-title {
...@@ -149,10 +135,7 @@ ...@@ -149,10 +135,7 @@
margin: 3px 0; margin: 3px 0;
} }
.encoding-selector, .encoding-selector {
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector {
display: block; display: block;
margin: 3px 0; margin: 3px 0;
...@@ -163,3 +146,104 @@ ...@@ -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 ...@@ -15,6 +15,9 @@ module IssuableCollections
# a new order into the collection. # a new order into the collection.
# We cannot use reorder to not mess up the paginated collection. # We cannot use reorder to not mess up the paginated collection.
issuable_ids = issuable_collection.map(&:id) issuable_ids = issuable_collection.map(&:id)
return {} if issuable_ids.empty?
issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type) issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
issuable_merge_requests_count = issuable_merge_requests_count =
......
...@@ -59,7 +59,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -59,7 +59,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
def render_ok def render_ok
set_workhorse_internal_api_content_type 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 end
def render_http_not_allowed def render_http_not_allowed
......
...@@ -164,11 +164,6 @@ module Ci ...@@ -164,11 +164,6 @@ module Ci
builds.latest.with_artifacts_not_expired.includes(project: [:namespace]) builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end 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 def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)") self.errors.add(:sha, " cant be 00000000 (branch removal)")
......
...@@ -60,16 +60,25 @@ class NotificationSetting < ActiveRecord::Base ...@@ -60,16 +60,25 @@ class NotificationSetting < ActiveRecord::Base
def set_events def set_events
return if custom? return if custom?
EMAIL_EVENTS.each do |event| self.events = {}
events[event] = false
end
end end
# Validates store accessors values as boolean # Validates store accessors values as boolean
# It is a text field so it does not cast correct boolean values in JSON # It is a text field so it does not cast correct boolean values in JSON
def events_to_boolean def events_to_boolean
EMAIL_EVENTS.each do |event| 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
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 end
...@@ -22,22 +22,21 @@ class KubernetesService < DeploymentService ...@@ -22,22 +22,21 @@ class KubernetesService < DeploymentService
with_options presence: true, if: :activated? do with_options presence: true, if: :activated? do
validates :api_url, url: true validates :api_url, url: true
validates :token validates :token
end
validates :namespace, validates :namespace,
allow_blank: true,
length: 1..63,
if: :activated?,
format: { format: {
with: Gitlab::Regex.kubernetes_namespace_regex, with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message, message: Gitlab::Regex.kubernetes_namespace_regex_message
}, }
length: 1..63
end
after_save :clear_reactive_cache! after_save :clear_reactive_cache!
def initialize_properties def initialize_properties
if properties.nil? self.properties = {} if properties.nil?
self.properties = {}
self.namespace = "#{project.path}-#{project.id}" if project.present?
end
end end
def title def title
...@@ -62,7 +61,7 @@ class KubernetesService < DeploymentService ...@@ -62,7 +61,7 @@ class KubernetesService < DeploymentService
{ type: 'text', { type: 'text',
name: 'namespace', name: 'namespace',
title: 'Kubernetes namespace', title: 'Kubernetes namespace',
placeholder: 'Kubernetes namespace' }, placeholder: namespace_placeholder },
{ type: 'text', { type: 'text',
name: 'api_url', name: 'api_url',
title: 'API URL', title: 'API URL',
...@@ -92,7 +91,7 @@ class KubernetesService < DeploymentService ...@@ -92,7 +91,7 @@ class KubernetesService < DeploymentService
variables = [ variables = [
{ key: 'KUBE_URL', value: api_url, public: true }, { key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false }, { 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? if ca_pem.present?
...@@ -132,8 +131,26 @@ class KubernetesService < DeploymentService ...@@ -132,8 +131,26 @@ class KubernetesService < DeploymentService
{ pods: read_pods, deployments: read_deployments } { pods: read_pods, deployments: read_deployments }
end end
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
private 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') def build_kubeclient!(api_path: 'api', api_version: 'v1')
raise "Incomplete settings" unless api_url && namespace && token raise "Incomplete settings" unless api_url && namespace && token
......
class SystemNoteMetadata < ActiveRecord::Base class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[ ICON_TYPES = %w[
commit merge confidentiality status label assignee cross_reference commit merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved approvals title time_tracking branch milestone discussion task moved opened closed merged
approvals
].freeze ].freeze
validates :note, presence: true validates :note, presence: true
......
...@@ -24,6 +24,10 @@ class BaseService ...@@ -24,6 +24,10 @@ class BaseService
Gitlab::AppLogger.info message Gitlab::AppLogger.info message
end end
def log_error(message)
Gitlab::AppLogger.error message
end
def system_hook_service def system_hook_service
SystemHooksService.new SystemHooksService.new
end end
......
...@@ -5,8 +5,6 @@ module Ci ...@@ -5,8 +5,6 @@ module Ci
def execute(pipeline) def execute(pipeline)
@pipeline = pipeline @pipeline = pipeline
ensure_created_builds! # TODO, remove me in 9.0
new_builds = new_builds =
stage_indexes_of_created_builds.map do |index| stage_indexes_of_created_builds.map do |index|
process_stage(index) process_stage(index)
...@@ -73,18 +71,5 @@ module Ci ...@@ -73,18 +71,5 @@ module Ci
def created_builds def created_builds
pipeline.builds.created pipeline.builds.created
end 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
end end
...@@ -12,11 +12,7 @@ class NotificationRecipientService ...@@ -12,11 +12,7 @@ class NotificationRecipientService
custom_action = build_custom_key(action, target) custom_action = build_custom_key(action, target)
recipients = target.participants(current_user) recipients = target.participants(current_user)
unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
recipients = add_project_watchers(recipients) recipients = add_project_watchers(recipients)
end
recipients = add_custom_notifications(recipients, custom_action) recipients = add_custom_notifications(recipients, custom_action)
recipients = reject_mention_users(recipients) recipients = reject_mention_users(recipients)
...@@ -43,6 +39,28 @@ class NotificationRecipientService ...@@ -43,6 +39,28 @@ class NotificationRecipientService
recipients.uniq recipients.uniq
end 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:) def build_relabeled_recipients(target, current_user, labels:)
recipients = add_labels_subscribers([], target, labels: labels) recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target) recipients = reject_unsubscribed_users(recipients, target)
...@@ -290,4 +308,16 @@ class NotificationRecipientService ...@@ -290,4 +308,16 @@ class NotificationRecipientService
def build_custom_key(action, object) def build_custom_key(action, object)
"#{action}_#{object.class.model_name.name.underscore}".to_sym "#{action}_#{object.class.model_name.name.underscore}".to_sym
end 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 end
...@@ -295,11 +295,11 @@ class NotificationService ...@@ -295,11 +295,11 @@ class NotificationService
return unless mailer.respond_to?(email_template) return unless mailer.respond_to?(email_template)
recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients( recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients(
pipeline, pipeline,
pipeline.user, pipeline.user,
action: pipeline.status, action: pipeline.status,
skip_current_user: false).map(&:notification_email) ).map(&:notification_email)
if recipients.any? if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later mailer.public_send(email_template, pipeline, recipients).deliver_later
......
...@@ -46,6 +46,7 @@ module Projects ...@@ -46,6 +46,7 @@ module Projects
end end
def error(message, http_status = nil) def error(message, http_status = nil)
log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest? @status.allow_failure = !latest?
@status.description = message @status.description = message
@status.drop @status.drop
......
...@@ -183,7 +183,9 @@ module SystemNoteService ...@@ -183,7 +183,9 @@ module SystemNoteService
body = status.dup body = status.dup
body << " via #{source.gfm_reference(project)}" if source 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 end
# Called when 'merge when pipeline succeeds' is executed # Called when 'merge when pipeline succeeds' is executed
...@@ -273,9 +275,15 @@ module SystemNoteService ...@@ -273,9 +275,15 @@ module SystemNoteService
# #
# Returns the created Note object # Returns the created Note object
def change_issue_confidentiality(issue, project, author) 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 end
# Called when a branch in Noteable is changed # Called when a branch in Noteable is changed
......
- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create'
.file-holder.file.append-bottom-default .file-holder.file.append-bottom-default
.js-file-title.file-title.clearfix .js-file-title.file-title.clearfix{ data: { current_action: action } }
.editor-ref .editor-ref
= icon('code-fork') = icon('code-fork')
= ref = ref
%span.editor-file-name %span.editor-file-name
- if current_action?(:edit) || current_action?(:update) - if current_action?(:edit) || current_action?(:update)
= text_field_tag 'file_path', (params[:file_path] || @path), = 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) - if current_action?(:new) || current_action?(:create)
%span.editor-file-name %span.editor-file-name
\/ \/
= text_field_tag 'file_name', params[:file_name], placeholder: "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 .pull-right.file-buttons
.license-selector.js-license-selector-wrap.hidden = button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do
= 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
%span.no-wrap %span.no-wrap
= custom_icon('icon_no_wrap') = custom_icon('icon_no_wrap')
No wrap No wrap
...@@ -31,7 +25,7 @@ ...@@ -31,7 +25,7 @@
= custom_icon('icon_soft_wrap') = custom_icon('icon_soft_wrap')
Soft wrap Soft wrap
.encoding-selector .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 .file-editor.code
%pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data] %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 @@ ...@@ -11,12 +11,15 @@
Someone edited the file the same time you did. Please check out 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' = 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. 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 .file-editor
%ul.nav-links.no-bottom.js-edit-mode %ul.nav-links.no-bottom.js-edit-mode
%li.active %li.active
= link_to '#editor' do = link_to '#editor' do
Edit File Write
%li %li
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js') = page_specific_javascript_tag('lib/ace.js')
= page_specific_javascript_bundle_tag('blob') = page_specific_javascript_bundle_tag('blob')
.editor-title-row
%h3.page-title %h3.page-title.blob-new-page-title
New File New file
= render 'template_selectors'
.file-editor .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 = 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 = render 'projects/blob/editor', ref: @ref
......
...@@ -249,6 +249,8 @@ ...@@ -249,6 +249,8 @@
%ul %ul
%li Be careful. Renaming a project's repository can have unintended side effects. %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. %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" = f.submit 'Rename project', class: "btn btn-warning"
- if can?(current_user, :change_namespace, @project) - if can?(current_user, :change_namespace, @project)
%hr %hr
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
.form-group .form-group
.checkbox{ class: ("prepend-top-0" if index == 0) } .checkbox{ class: ("prepend-top-0" if index == 0) }
%label{ for: field_id } %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 %strong
= notification_event_name(event) = notification_event_name(event)
= icon("spinner spin", class: "custom-notification-event-loading") = icon("spinner spin", class: "custom-notification-event-loading")
...@@ -53,6 +53,8 @@ class ProcessCommitWorker ...@@ -53,6 +53,8 @@ class ProcessCommitWorker
def update_issue_metrics(commit, author) def update_issue_metrics(commit, author)
mentioned_issues = commit.all_references(author).issues 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). 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) update_all(first_mentioned_in_commit_at: commit.committed_date)
end 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 ...@@ -548,7 +548,7 @@ production: &base
storages: # You must have at least a `default` storage path. storages: # You must have at least a `default` storage path.
default: default:
path: /home/git/repositories/ 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 settings
backup: backup:
......
...@@ -9,7 +9,7 @@ if Gitlab.config.gitaly.enabled || Rails.env.test? ...@@ -9,7 +9,7 @@ if Gitlab.config.gitaly.enabled || Rails.env.test?
raise "storage #{name.inspect} is missing a gitaly_address" raise "storage #{name.inspect} is missing a gitaly_address"
end end
unless URI(address).scheme == 'unix' unless URI(address).scheme.in?(%w(tcp unix))
raise "Unsupported Gitaly address: #{address.inspect}" raise "Unsupported Gitaly address: #{address.inspect}"
end end
......
if ENV['ENABLE_BULLET'] if defined?(Bullet) && ENV['ENABLE_BULLET']
require 'bullet' Rails.application.configure do
config.after_initialize do
Bullet.enable = true Bullet.enable = true
Bullet.bullet_logger = true
Bullet.console = true Bullet.console = true
Bullet.raise = Rails.env.test?
end
end
end end
...@@ -4,10 +4,10 @@ module Db ...@@ -4,10 +4,10 @@ module Db
class AbuseReport class AbuseReport
def self.seed def self.seed
Gitlab::Seeder.quiet do Gitlab::Seeder.quiet do
(::AbuseReport.default_per_page + 3).times do (::AbuseReport.default_per_page + 3).times do |i|
reported_user = reported_user =
::User.create!( ::User.create!(
username: FFaker::Internet.user_name, username: "reported_user_#{i}",
name: FFaker::Name.name, name: FFaker::Name.name,
email: FFaker::Internet.email, email: FFaker::Internet.email,
confirmed_at: DateTime.now, 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 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1453,7 +1453,6 @@ ActiveRecord::Schema.define(version: 20170329124448) do ...@@ -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", ["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", ["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", ["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", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"} 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 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. ...@@ -22,6 +22,9 @@ Instead you should use polling mechanism with ETag caching in Redis.
## How it works ## 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 1. Whenever a resource changes we generate a random value and store it in
Redis. Redis.
1. When a client makes a request we set the `ETag` response header to the value 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 ...@@ -57,7 +57,7 @@ sudo -u git -H bundle clean
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache # 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 ### 4. Update gitlab-workhorse to the corresponding version
......
...@@ -157,6 +157,11 @@ the same goal: ...@@ -157,6 +157,11 @@ the same goal:
where `PROJECT-1` is the issue ID of the JIRA project. 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 ### JIRA issue closing example
Let's consider the following example: Let's consider the following example:
......
...@@ -66,14 +66,13 @@ Below is the table of events users can be notified of: ...@@ -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: In all of the below cases, the notification will be sent to:
- Participants: - Participants:
- the author and assignee of the issue/merge request - the author and assignee of the issue/merge request
- the author of the pipeline
- authors of comments on the issue/merge request - 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 the issue/merge request title or description
- anyone mentioned by `@username` in any of the comments on the issue/merge request - anyone mentioned by `@username` in any of the comments on the issue/merge request
...with notification level "Participating" or higher ...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 - 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 - 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: ...@@ -91,8 +90,8 @@ In all of the below cases, the notification will be sent to:
| Approved merge request | | | Approved merge request | |
| Unapproved merge request | | | Unapproved merge request | |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher | | 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 | | Failed pipeline | The author of the pipeline |
| Successful pipeline | The above, plus 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 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 changed, notifications will be sent to any **new** mentions by `@username` as
......
...@@ -24,8 +24,5 @@ Capybara.ignore_hidden_elements = false ...@@ -24,8 +24,5 @@ Capybara.ignore_hidden_elements = false
Capybara::Screenshot.prune_strategy = :keep_last_run Capybara::Screenshot.prune_strategy = :keep_last_run
Spinach.hooks.before_run do Spinach.hooks.before_run do
require 'spinach/capybara'
require 'capybara/rails'
TestEnv.eager_load_driver_server TestEnv.eager_load_driver_server
end end
...@@ -5,10 +5,6 @@ ENV['RAILS_ENV'] = 'test' ...@@ -5,10 +5,6 @@ ENV['RAILS_ENV'] = 'test'
require './config/environment' require './config/environment'
require 'rspec/expectations' require 'rspec/expectations'
require_relative 'capybara'
require_relative 'db_cleaner'
require_relative 'rerun'
if ENV['CI'] if ENV['CI']
require 'knapsack' require 'knapsack'
Knapsack::Adapters::SpinachAdapter.bind Knapsack::Adapters::SpinachAdapter.bind
......
...@@ -50,10 +50,14 @@ module API ...@@ -50,10 +50,14 @@ module API
forbidden!('Job has been erased!') if job.erased? forbidden!('Job has been erased!') if job.erased?
end end
def authenticate_job!(job) def authenticate_job!
job = Ci::Build.find_by_id(params[:id])
validate_job!(job) do validate_job!(job) do
forbidden! unless job_token_valid?(job) forbidden! unless job_token_valid?(job)
end end
job
end end
def job_token_valid?(job) def job_token_valid?(job)
......
...@@ -113,8 +113,7 @@ module API ...@@ -113,8 +113,7 @@ module API
optional :state, type: String, desc: %q(Job's status: success, failed) optional :state, type: String, desc: %q(Job's status: success, failed)
end end
put '/:id' do put '/:id' do
job = Ci::Build.find_by_id(params[:id]) job = authenticate_job!
authenticate_job!(job)
job.update_attributes(trace: params[:trace]) if params[:trace] job.update_attributes(trace: params[:trace]) if params[:trace]
...@@ -140,8 +139,7 @@ module API ...@@ -140,8 +139,7 @@ module API
optional :token, type: String, desc: %q(Job's authentication token) optional :token, type: String, desc: %q(Job's authentication token)
end end
patch '/:id/trace' do patch '/:id/trace' do
job = Ci::Build.find_by_id(params[:id]) job = authenticate_job!
authenticate_job!(job)
error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range') error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
content_range = request.headers['Content-Range'] content_range = request.headers['Content-Range']
...@@ -175,8 +173,7 @@ module API ...@@ -175,8 +173,7 @@ module API
require_gitlab_workhorse! require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers) Gitlab::Workhorse.verify_api_request!(headers)
job = Ci::Build.find_by_id(params[:id]) job = authenticate_job!
authenticate_job!(job)
forbidden!('Job is not running') unless job.running? forbidden!('Job is not running') unless job.running?
if params[:filesize] if params[:filesize]
...@@ -212,8 +209,7 @@ module API ...@@ -212,8 +209,7 @@ module API
not_allowed! unless Gitlab.config.artifacts.enabled not_allowed! unless Gitlab.config.artifacts.enabled
require_gitlab_workhorse! require_gitlab_workhorse!
job = Ci::Build.find_by_id(params[:id]) job = authenticate_job!
authenticate_job!(job)
forbidden!('Job is not running!') unless job.running? forbidden!('Job is not running!') unless job.running?
artifacts_upload_path = ArtifactUploader.artifacts_upload_path artifacts_upload_path = ArtifactUploader.artifacts_upload_path
...@@ -245,8 +241,7 @@ module API ...@@ -245,8 +241,7 @@ module API
optional :token, type: String, desc: %q(Job's authentication token) optional :token, type: String, desc: %q(Job's authentication token)
end end
get '/:id/artifacts' do get '/:id/artifacts' do
job = Ci::Build.find_by_id(params[:id]) job = authenticate_job!
authenticate_job!(job)
artifacts_file = job.artifacts_file artifacts_file = job.artifacts_file
unless artifacts_file.file_storage? unless artifacts_file.file_storage?
......
...@@ -86,8 +86,7 @@ module Ci ...@@ -86,8 +86,7 @@ module Ci
# Example Request: # Example Request:
# PATCH /builds/:id/trace.txt # PATCH /builds/:id/trace.txt
patch ":id/trace.txt" do patch ":id/trace.txt" do
build = Ci::Build.find_by_id(params[:id]) build = authenticate_build!
authenticate_build!(build)
error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range') error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
content_range = request.headers['Content-Range'] content_range = request.headers['Content-Range']
...@@ -117,8 +116,7 @@ module Ci ...@@ -117,8 +116,7 @@ module Ci
require_gitlab_workhorse! require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers) Gitlab::Workhorse.verify_api_request!(headers)
not_allowed! unless Gitlab.config.artifacts.enabled not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id]) build = authenticate_build!
authenticate_build!(build)
forbidden!('build is not running') unless build.running? forbidden!('build is not running') unless build.running?
if params[:filesize] if params[:filesize]
...@@ -154,8 +152,7 @@ module Ci ...@@ -154,8 +152,7 @@ module Ci
post ":id/artifacts" do post ":id/artifacts" do
require_gitlab_workhorse! require_gitlab_workhorse!
not_allowed! unless Gitlab.config.artifacts.enabled not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id]) build = authenticate_build!
authenticate_build!(build)
forbidden!('Build is not running!') unless build.running? forbidden!('Build is not running!') unless build.running?
artifacts_upload_path = ArtifactUploader.artifacts_upload_path artifacts_upload_path = ArtifactUploader.artifacts_upload_path
...@@ -189,8 +186,7 @@ module Ci ...@@ -189,8 +186,7 @@ module Ci
# Example Request: # Example Request:
# GET /builds/:id/artifacts # GET /builds/:id/artifacts
get ":id/artifacts" do get ":id/artifacts" do
build = Ci::Build.find_by_id(params[:id]) build = authenticate_build!
authenticate_build!(build)
artifacts_file = build.artifacts_file artifacts_file = build.artifacts_file
unless artifacts_file.file_storage? unless artifacts_file.file_storage?
...@@ -214,8 +210,7 @@ module Ci ...@@ -214,8 +210,7 @@ module Ci
# Example Request: # Example Request:
# DELETE /builds/:id/artifacts # DELETE /builds/:id/artifacts
delete ":id/artifacts" do delete ":id/artifacts" do
build = Ci::Build.find_by_id(params[:id]) build = authenticate_build!
authenticate_build!(build)
status(200) status(200)
build.erase_artifacts! build.erase_artifacts!
......
...@@ -13,10 +13,14 @@ module Ci ...@@ -13,10 +13,14 @@ module Ci
forbidden! unless current_runner forbidden! unless current_runner
end end
def authenticate_build!(build) def authenticate_build!
build = Ci::Build.find_by_id(params[:id])
validate_build!(build) do validate_build!(build) do
forbidden! unless build_token_valid?(build) forbidden! unless build_token_valid?(build)
end end
build
end end
def validate_build!(build) def validate_build!(build)
......
...@@ -12,9 +12,11 @@ module Gitlab ...@@ -12,9 +12,11 @@ module Gitlab
end end
def self.new_channel(address) 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 # handled using the file system and no additional authentication is
# required (therefore the :this_channel_is_insecure flag) # 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) GRPC::Core::Channel.new(address, {}, :this_channel_is_insecure)
end end
......
...@@ -236,7 +236,10 @@ module Gitlab ...@@ -236,7 +236,10 @@ module Gitlab
# add_namespace("/path/to/storage", "gitlab") # add_namespace("/path/to/storage", "gitlab")
# #
def add_namespace(storage, name) 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 end
# Remove directory from repositories storage # Remove directory from repositories storage
......
...@@ -16,7 +16,7 @@ module Gitlab ...@@ -16,7 +16,7 @@ module Gitlab
SECRET_LENGTH = 32 SECRET_LENGTH = 32
class << self class << self
def git_http_ok(repository, user) def git_http_ok(repository, user, action)
repo_path = repository.path_to_repo repo_path = repository.path_to_repo
params = { params = {
GL_ID: Gitlab::GlId.gl_id(user), GL_ID: Gitlab::GlId.gl_id(user),
...@@ -26,13 +26,25 @@ module Gitlab ...@@ -26,13 +26,25 @@ module Gitlab
if Gitlab.config.gitaly.enabled if Gitlab.config.gitaly.enabled
storage = repository.project.repository_storage storage = repository.project.repository_storage
address = Gitlab::GitalyClient.get_address(storage) address = Gitlab::GitalyClient.get_address(storage)
params[:GitalySocketPath] = URI(address).path
# TODO: use GitalyClient code to assemble the Repository message # TODO: use GitalyClient code to assemble the Repository message
params[:Repository] = Gitaly::Repository.new( params[:Repository] = Gitaly::Repository.new(
path: repo_path, path: repo_path,
storage_name: storage, storage_name: storage,
relative_path: Gitlab::RepoPath.strip_storage_path(repo_path), relative_path: Gitlab::RepoPath.strip_storage_path(repo_path),
).to_h ).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 end
params params
......
...@@ -48,6 +48,10 @@ FactoryGirl.define do ...@@ -48,6 +48,10 @@ FactoryGirl.define do
trait :success do trait :success do
status :success status :success
end end
trait :failed do
status :failed
end
end end
end end
end end
...@@ -42,7 +42,7 @@ describe 'Auto deploy' do ...@@ -42,7 +42,7 @@ describe 'Auto deploy' do
it 'includes OpenShift as an available template', js: true do it 'includes OpenShift as an available template', js: true do
click_link 'Set up auto deploy' 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 within '.gitlab-ci-yml-selector' do
expect(page).to have_content('OpenShift') expect(page).to have_content('OpenShift')
...@@ -51,7 +51,7 @@ describe 'Auto deploy' do ...@@ -51,7 +51,7 @@ describe 'Auto deploy' do
it 'creates a merge request using "auto-deploy" branch', js: true do it 'creates a merge request using "auto-deploy" branch', js: true do
click_link 'Set up auto deploy' 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 within '.gitlab-ci-yml-selector' do
click_on 'OpenShift' click_on 'OpenShift'
end end
......
...@@ -6,8 +6,8 @@ describe 'Filter issues', js: true, feature: true do ...@@ -6,8 +6,8 @@ describe 'Filter issues', js: true, feature: true do
let!(:group) { create(:group) } let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) } let!(:project) { create(:project, group: group) }
let!(:user) { create(:user) } let!(:user) { create(:user, username: 'joe') }
let!(:user2) { create(:user) } let!(:user2) { create(:user, username: 'jane') }
let!(:label) { create(:label, project: project) } let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") } let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
...@@ -70,7 +70,7 @@ describe 'Filter issues', js: true, feature: true do ...@@ -70,7 +70,7 @@ describe 'Filter issues', js: true, feature: true do
issue_with_caps_label.labels << caps_sensitive_label issue_with_caps_label.labels << caps_sensitive_label
issue_with_everything = create(:issue, issue_with_everything = create(:issue,
title: "Bug report with everything you thought was possible", title: "Bug report foo was possible",
project: project, project: project,
milestone: milestone, milestone: milestone,
author: user, author: user,
...@@ -687,10 +687,10 @@ describe 'Filter issues', js: true, feature: true do ...@@ -687,10 +687,10 @@ describe 'Filter issues', js: true, feature: true do
end end
it 'filters issues by searched text, author, more text, assignee and even more text' do 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_issues_list_count(1)
expect_filtered_search_input('bug report with') expect_filtered_search_input('bug report foo')
end end
it 'filters issues by searched text, author, assignee and label' do it 'filters issues by searched text, author, assignee and label' do
...@@ -701,10 +701,10 @@ describe 'Filter issues', js: true, feature: true do ...@@ -701,10 +701,10 @@ describe 'Filter issues', js: true, feature: true do
end end
it 'filters issues by searched text, author, text, assignee, text, label and text' do 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_issues_list_count(1)
expect_filtered_search_input('bug report with everything') expect_filtered_search_input('bug report foo')
end end
it 'filters issues by searched text, author, assignee, label and milestone' do it 'filters issues by searched text, author, assignee, label and milestone' do
...@@ -715,10 +715,10 @@ describe 'Filter issues', js: true, feature: true do ...@@ -715,10 +715,10 @@ describe 'Filter issues', js: true, feature: true do
end end
it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do 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_issues_list_count(1)
expect_filtered_search_input('bug report with everything you') expect_filtered_search_input('bug report foo')
end end
it 'filters issues by searched text, author, assignee, multiple labels and milestone' do 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 ...@@ -729,10 +729,10 @@ describe 'Filter issues', js: true, feature: true do
end end
it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do 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_issues_list_count(1)
expect_filtered_search_input('bug report with everything you thought') expect_filtered_search_input('bug report foo')
end end
end end
......
...@@ -88,7 +88,7 @@ feature 'New blob creation', feature: true, js: true do ...@@ -88,7 +88,7 @@ feature 'New blob creation', feature: true, js: true do
scenario 'shows error message' 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('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') expect(page).to have_content('NextFeature')
end end
end end
......
...@@ -40,7 +40,7 @@ feature 'project owner creates a license file', feature: true, js: true do ...@@ -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 scenario 'project master creates a license file from the "Add license" link' do
click_link 'Add License' click_link 'Add License'
expect(page).to have_content('New File') expect(page).to have_content('New file')
expect(current_path).to eq( expect(current_path).to eq(
namespace_project_new_blob_path(project.namespace, project, 'master')) namespace_project_new_blob_path(project.namespace, project, 'master'))
expect(find('#file_name').value).to eq('LICENSE') expect(find('#file_name').value).to eq('LICENSE')
...@@ -63,7 +63,7 @@ feature 'project owner creates a license file', feature: true, js: true do ...@@ -63,7 +63,7 @@ feature 'project owner creates a license file', feature: true, js: true do
def select_template(template) def select_template(template)
page.within('.js-license-selector-wrap') do page.within('.js-license-selector-wrap') do
click_button 'Choose a License template' click_button 'Apply a License template'
click_link template click_link template
wait_for_ajax wait_for_ajax
end end
......
...@@ -14,7 +14,7 @@ feature 'project owner sees a link to create a license file in empty project', f ...@@ -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) visit namespace_project_path(project.namespace, project)
click_link 'Create empty bare repository' click_link 'Create empty bare repository'
click_on 'LICENSE' click_on 'LICENSE'
expect(page).to have_content('New File') expect(page).to have_content('New file')
expect(current_path).to eq( expect(current_path).to eq(
namespace_project_new_blob_path(project.namespace, project, 'master')) 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 ...@@ -40,7 +40,7 @@ feature 'project owner sees a link to create a license file in empty project', f
def select_template(template) def select_template(template)
page.within('.js-license-selector-wrap') do page.within('.js-license-selector-wrap') do
click_button 'Choose a License template' click_button 'Apply a License template'
click_link template click_link template
wait_for_ajax wait_for_ajax
end 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 ...@@ -180,24 +180,68 @@ describe Gitlab::Workhorse, lib: true do
describe '.git_http_ok' do describe '.git_http_ok' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:repo_path) { repository.path_to_repo } 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 context 'when Gitaly is enabled' do
let(:gitaly_params) do
{
GitalySocketPath: URI(Gitlab::GitalyClient.get_address('default')).path,
}
end
before do before do
allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
end end
it 'includes Gitaly params in the returned value' do it 'includes a Repository param' do
gitaly_socket_path = URI(Gitlab::GitalyClient.get_address('default')).path repo_param = { Repository: {
expect(subject).to include({ GitalySocketPath: gitaly_socket_path })
expect(subject[:Repository]).to include({
path: repo_path, path: repo_path,
storage_name: 'default', storage_name: 'default',
relative_path: project.full_path + '.git', 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 end
end end
......
...@@ -1055,10 +1055,13 @@ describe Ci::Pipeline, models: true do ...@@ -1055,10 +1055,13 @@ describe Ci::Pipeline, models: true do
end end
before do before do
reset_delivered_emails!
project.team << [pipeline.user, Gitlab::Access::DEVELOPER] 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 perform_enqueued_jobs do
pipeline.enqueue pipeline.enqueue
pipeline.run pipeline.run
......
...@@ -4,7 +4,7 @@ describe KubernetesService, models: true, caching: true do ...@@ -4,7 +4,7 @@ describe KubernetesService, models: true, caching: true do
include KubernetesHelpers include KubernetesHelpers
include ReactiveCachingHelpers include ReactiveCachingHelpers
let(:project) { create(:kubernetes_project) } let(:project) { build_stubbed(:kubernetes_project) }
let(:service) { project.kubernetes_service } let(:service) { project.kubernetes_service }
describe "Associations" do describe "Associations" do
...@@ -14,7 +14,8 @@ describe KubernetesService, models: true, caching: true do ...@@ -14,7 +14,8 @@ describe KubernetesService, models: true, caching: true do
describe 'Validations' do describe 'Validations' do
context 'when service is active' do context 'when service is active' do
before { subject.active = true } 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(:api_url) }
it { is_expected.to validate_presence_of(:token) } it { is_expected.to validate_presence_of(:token) }
...@@ -37,7 +38,7 @@ describe KubernetesService, models: true, caching: true do ...@@ -37,7 +38,7 @@ describe KubernetesService, models: true, caching: true do
'a.b' => false, 'a.b' => false,
'a*b' => false, 'a*b' => false,
}.each do |namespace, validity| }.each do |namespace, validity|
it "should validate #{namespace} as #{validity ? 'valid' : 'invalid'}" do it "validates #{namespace} as #{validity ? 'valid' : 'invalid'}" do
subject.namespace = namespace subject.namespace = namespace
expect(subject.valid?).to eq(validity) expect(subject.valid?).to eq(validity)
...@@ -48,24 +49,40 @@ describe KubernetesService, models: true, caching: true do ...@@ -48,24 +49,40 @@ describe KubernetesService, models: true, caching: true do
context 'when service is inactive' do context 'when service is inactive' do
before { subject.active = false } 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(:api_url) }
it { is_expected.not_to validate_presence_of(:token) } it { is_expected.not_to validate_presence_of(:token) }
end end
end end
describe '#initialize_properties' do describe '#initialize_properties' do
context 'with a project' do context 'without a project' do
let(:namespace_name) { "#{project.path}-#{project.id}" } 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 describe '#fields' do
expect(described_class.new(project: project).namespace).to eq(namespace_name) 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
end end
context 'without a project' do context 'with associated project' do
it 'leaves the namespace unset' do before { subject.project = project }
expect(described_class.new.namespace).to be_nil
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 end
end end
...@@ -123,38 +140,40 @@ describe KubernetesService, models: true, caching: true do ...@@ -123,38 +140,40 @@ describe KubernetesService, models: true, caching: true do
before do before do
subject.api_url = 'https://kube.domain.com' subject.api_url = 'https://kube.domain.com'
subject.token = 'token' subject.token = 'token'
subject.namespace = 'my-project'
subject.ca_pem = 'CA PEM DATA' subject.ca_pem = 'CA PEM DATA'
subject.project = project
end end
it 'sets KUBE_URL' do context 'namespace is provided' do
expect(subject.predefined_variables).to include( before { subject.namespace = 'my-project' }
{ key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }
)
end
it 'sets KUBE_TOKEN' do it 'sets the variables' do
expect(subject.predefined_variables).to include( 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 end
it 'sets KUBE_NAMESPACE' do
expect(subject.predefined_variables).to include(
{ key: 'KUBE_NAMESPACE', value: 'my-project', public: true }
)
end end
it 'sets KUBE_CA_PEM' do context 'no namespace provided' do
it 'sets the variables' do
expect(subject.predefined_variables).to include( 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 end
it 'sets KUBE_CA_PEM_FILE' do it 'sets the KUBE_NAMESPACE' do
expect(subject.predefined_variables).to include( kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
{ key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
) expect(kube_namespace).not_to be_nil
expect(kube_namespace[:value]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
end
end end
end end
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::Members, api: true do describe API::Members, api: true do
include ApiHelpers include ApiHelpers
let(:master) { create(:user) } let(:master) { create(:user, username: 'master_user') }
let(:developer) { create(:user) } let(:developer) { create(:user) }
let(:access_requester) { create(:user) } let(:access_requester) { create(:user) }
let(:stranger) { create(:user) } let(:stranger) { create(:user) }
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::V3::Members, api: true do describe API::V3::Members, api: true do
include ApiHelpers include ApiHelpers
let(:master) { create(:user) } let(:master) { create(:user, username: 'master_user') }
let(:developer) { create(:user) } let(:developer) { create(:user) }
let(:access_requester) { create(:user) } let(:access_requester) { create(:user) }
let(:stranger) { create(:user) } let(:stranger) { create(:user) }
......
...@@ -418,65 +418,6 @@ describe Ci::ProcessPipelineService, '#execute', :services do ...@@ -418,65 +418,6 @@ describe Ci::ProcessPipelineService, '#execute', :services do
end end
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 def process_pipeline
described_class.new(pipeline.project, user).execute(pipeline) described_class.new(pipeline.project, user).execute(pipeline)
end end
......
This diff is collapsed.
...@@ -221,26 +221,23 @@ describe SystemNoteService, services: true do ...@@ -221,26 +221,23 @@ describe SystemNoteService, services: true do
describe '.change_status' do describe '.change_status' do
subject { described_class.change_status(noteable, project, author, status, source) } 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 } let(:source) { nil }
it_behaves_like 'a system note' do it_behaves_like 'a system note' do
let(:action) { 'status' } let(:action) { 'opened' }
end
end end
context 'with a source' do context 'with a source' do
let(:status) { 'opened' }
let(:source) { double('commit', gfm_reference: 'commit 123456') } let(:source) { double('commit', gfm_reference: 'commit 123456') }
it 'sets the note text' do it 'sets the note text' do
expect(subject.note).to eq "#{status} via commit 123456" expect(subject.note).to eq "#{status} via commit 123456"
end end
end end
context 'without a source' do
it 'sets the note text' do
expect(subject.note).to eq status
end
end
end end
describe '.merge_when_pipeline_succeeds' do describe '.merge_when_pipeline_succeeds' do
...@@ -298,9 +295,23 @@ describe SystemNoteService, services: true do ...@@ -298,9 +295,23 @@ describe SystemNoteService, services: true do
describe '.change_issue_confidentiality' do describe '.change_issue_confidentiality' do
subject { described_class.change_issue_confidentiality(noteable, project, author) } 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 it_behaves_like 'a system note' do
let(:action) { 'confidentiality' } let(:action) { 'visible' }
end end
it 'sets the note text' do it 'sets the note text' do
......
...@@ -33,4 +33,19 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil| ...@@ -33,4 +33,19 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
expect(meta_data[id].upvotes).to eq(id + 2) expect(meta_data[id].upvotes).to eq(id + 2)
end end
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 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' ...@@ -3,131 +3,19 @@ require 'spec_helper'
describe PipelineNotificationWorker do describe PipelineNotificationWorker do
include EmailHelpers include EmailHelpers
let(:pipeline) do let(:pipeline) { create(:ci_pipeline) }
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 }
describe '#execute' do describe '#execute' do
before do it 'calls NotificationService#pipeline_finished when the pipeline exists' do
reset_delivered_emails! expect(NotificationService).to receive_message_chain(:new, :pipeline_finished)
pipeline.project.team << [pusher, Gitlab::Access::DEVELOPER]
end
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) subject.perform(pipeline.id)
end end
emails = ActionMailer::Base.deliveries it 'does nothing when the pipeline does not exist' do
actual = emails.flat_map(&:bcc).sort expect(NotificationService).not_to receive(:new)
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'
context 'with pipeline from someone else' do subject.perform(Ci::Pipeline.maximum(:id).to_i.succ)
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
end end
end end
end end
...@@ -99,6 +99,13 @@ describe ProcessCommitWorker do ...@@ -99,6 +99,13 @@ describe ProcessCommitWorker do
expect(metric.first_mentioned_in_commit_at).to eq(commit.committed_date) expect(metric.first_mentioned_in_commit_at).to eq(commit.committed_date)
end 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 end
describe '#build_commit' do 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