Commit 44321b1a authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into feature/multi-level-container-registry-images

* master: (57 commits)
  Ensure we generate unique usernames otherwise validations fail
  Fix a Knapsack issue that would load support/capybara.rb before support/env.rb
  Ensure users have a short username otherwise a click event is triggered ...
  Enable the `bullet_logger` setting; enable `raise` in test environment
  Fix Rubocop offenses
  Set the right timeout for Gitlab::Shell#fetch_remote
  Refactoring Projects::ImportService
  Move methods that are not related to mirroring to the repository model
  Fix GitHub pull request formatter spec
  Rename skip_metrics to imported on the importable concern
  Add CHANGELOG
  Remove unused include from RepositoryImportWorker
  Skip MR metrics when importing projects from GitHub
  Fetch GitHub project as a mirror to get all refs at once
  Make file templates easy to use and discover
  Ensure user has a unique username otherwise `user10` would match `user1`
  Ensure the AbuseReport fixtures create unique reported users
  Don't use FFaker in factories, use sequences instead
  Fix brittle specs
  Fix the AbuseReport seeder
  ...

Conflicts:
	db/schema.rb
parents b03f1699 5efd6794
...@@ -533,6 +533,10 @@ Style/WhileUntilModifier: ...@@ -533,6 +533,10 @@ Style/WhileUntilModifier:
Style/WordArray: Style/WordArray:
Enabled: true Enabled: true
# Use `proc` instead of `Proc.new`.
Style/Proc:
Enabled: true
# Metrics ##################################################################### # Metrics #####################################################################
# A calculated magnitude based on number of assignments, # A calculated magnitude based on number of assignments,
......
...@@ -226,11 +226,6 @@ Style/PredicateName: ...@@ -226,11 +226,6 @@ Style/PredicateName:
Style/PreferredHashMethods: Style/PreferredHashMethods:
Enabled: false Enabled: false
# Offense count: 8
# Cop supports --auto-correct.
Style/Proc:
Enabled: false
# Offense count: 62 # Offense count: 62
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
......
...@@ -260,7 +260,6 @@ group :development do ...@@ -260,7 +260,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
...@@ -272,6 +271,7 @@ group :development do ...@@ -272,6 +271,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'
...@@ -352,4 +352,4 @@ gem 'vmstat', '~> 2.3.0' ...@@ -352,4 +352,4 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.3.0' gem 'gitaly', '~> 0.5.0'
...@@ -253,7 +253,7 @@ GEM ...@@ -253,7 +253,7 @@ GEM
json json
get_process_mem (0.2.0) get_process_mem (0.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.3.0) gitaly (0.5.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -899,7 +899,7 @@ DEPENDENCIES ...@@ -899,7 +899,7 @@ DEPENDENCIES
fuubar (~> 2.0.0) fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0) gemojione (~> 3.0)
gitaly (~> 0.3.0) gitaly (~> 0.5.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
......
/* 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,29 +26,13 @@ export default class EditBlob { ...@@ -26,29 +26,13 @@ export default class EditBlob {
} }
} }
prepFileContentForSubmit() { initFileSelectors(currentAction) {
$('form').submit(() => { this.fileTemplateMediator = new TemplateSelectorMediator({
$('#file-content').val(this.editor.getValue()); currentAction,
editor: this.editor,
}); });
} }
initFileSelectors() {
this.blobTemplateSelectors = [
new BlobLicenseSelectors({
editor: this.editor,
}),
new BlobGitignoreSelectors({
editor: this.editor,
}),
new BlobCiYamlSelectors({
editor: this.editor,
}),
new BlobDockerfileSelectors({
editor: this.editor,
}),
];
}
initModePanesAndLinks() { initModePanesAndLinks() {
this.$editModePanes = $('.js-edit-mode-pane'); this.$editModePanes = $('.js-edit-mode-pane');
this.$editModeLinks = $('.js-edit-mode a'); this.$editModeLinks = $('.js-edit-mode a');
......
...@@ -263,7 +263,7 @@ ...@@ -263,7 +263,7 @@
}); });
/** /**
* Updates the search parameter of a URL given the parameter and values provided. * Updates the search parameter of a URL given the parameter and value provided.
* *
* If no search params are present we'll add it. * If no search params are present we'll add it.
* If param for page is already present, we'll update it * If param for page is already present, we'll update it
...@@ -278,17 +278,24 @@ ...@@ -278,17 +278,24 @@
let search; let search;
const locationSearch = window.location.search; const locationSearch = window.location.search;
if (locationSearch.length === 0) { if (locationSearch.length) {
search = `?${param}=${value}`; const parameters = locationSearch.substring(1, locationSearch.length)
} .split('&')
.reduce((acc, element) => {
const val = element.split('=');
acc[val[0]] = decodeURIComponent(val[1]);
return acc;
}, {});
if (locationSearch.indexOf(param) !== -1) { parameters[param] = value;
const regex = new RegExp(param + '=\\d');
search = locationSearch.replace(regex, `${param}=${value}`);
}
if (locationSearch.length && locationSearch.indexOf(param) === -1) { const toString = Object.keys(parameters)
search = `${locationSearch}&${param}=${value}`; .map(val => `${val}=${encodeURIComponent(parameters[val])}`)
.join('&');
search = `?${toString}`;
} else {
search = `?${param}=${value}`;
} }
return search; return search;
......
/* 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;
}
}
class Admin::AbuseReportsController < Admin::ApplicationController class Admin::AbuseReportsController < Admin::ApplicationController
def index def index
@abuse_reports = AbuseReport.order(id: :desc).page(params[:page]) @abuse_reports = AbuseReport.order(id: :desc).page(params[:page])
@abuse_reports.includes(:reporter, :user)
end end
def destroy def destroy
......
...@@ -134,6 +134,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -134,6 +134,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:unique_ips_limit_enabled, :unique_ips_limit_enabled,
:version_check_enabled, :version_check_enabled,
:terminal_max_session_time, :terminal_max_session_time,
:polling_interval_multiplier,
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
import_sources: [], import_sources: [],
......
...@@ -14,6 +14,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -14,6 +14,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@members = @members.search(params[:search]) if params[:search].present? @members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort) @members = @members.sort(@sort)
@members = @members.page(params[:page]).per(50) @members = @members.page(params[:page]).per(50)
@members.includes(:user)
@requesters = AccessRequestsFinder.new(@group).execute(current_user) @requesters = AccessRequestsFinder.new(@group).execute(current_user)
......
...@@ -57,7 +57,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -57,7 +57,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
......
...@@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@collection_type = "MergeRequest" @collection_type = "MergeRequest"
@merge_requests = merge_requests_collection @merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.includes(merge_request_diff: :merge_request)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
......
...@@ -21,9 +21,9 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -21,9 +21,9 @@ class Projects::MilestonesController < Projects::ApplicationController
@sort = params[:sort] || 'due_date_asc' @sort = params[:sort] || 'due_date_asc'
@milestones = @milestones.sort(@sort) @milestones = @milestones.sort(@sort)
@milestones = @milestones.includes(:project)
respond_to do |format| respond_to do |format|
format.html do format.html do
@milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page]) @milestones = @milestones.page(params[:page])
end end
format.json do format.json do
......
...@@ -25,12 +25,12 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -25,12 +25,12 @@ class RegistrationsController < Devise::RegistrationsController
end end
def destroy def destroy
Users::DestroyService.new(current_user).execute(current_user) DeleteUserWorker.perform_async(current_user.id, current_user.id)
respond_to do |format| respond_to do |format|
format.html do format.html do
session.try(:destroy) session.try(:destroy)
redirect_to new_user_session_path, notice: "Account successfully removed." redirect_to new_user_session_path, notice: "Account scheduled for removal."
end end
end end
end end
......
...@@ -79,7 +79,7 @@ class SessionsController < Devise::SessionsController ...@@ -79,7 +79,7 @@ class SessionsController < Devise::SessionsController
if request.referer.present? && (params['redirect_to_referer'] == 'yes') if request.referer.present? && (params['redirect_to_referer'] == 'yes')
referer_uri = URI(request.referer) referer_uri = URI(request.referer)
if referer_uri.host == Gitlab.config.gitlab.host if referer_uri.host == Gitlab.config.gitlab.host
referer_uri.path referer_uri.request_uri
else else
request.fullpath request.fullpath
end end
......
...@@ -5,8 +5,8 @@ class BaseMailer < ActionMailer::Base ...@@ -5,8 +5,8 @@ class BaseMailer < ActionMailer::Base
attr_accessor :current_user attr_accessor :current_user
helper_method :current_user, :can? helper_method :current_user, :can?
default from: Proc.new { default_sender_address.format } default from: proc { default_sender_address.format }
default reply_to: Proc.new { default_reply_to_address.format } default reply_to: proc { default_reply_to_address.format }
def can? def can?
Ability.allowed?(current_user, action, subject) Ability.allowed?(current_user, action, subject)
......
...@@ -131,6 +131,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -131,6 +131,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :polling_interval_multiplier,
presence: true,
numericality: { greater_than_or_equal_to: 0 }
validates_each :restricted_visibility_levels do |record, attr, value| validates_each :restricted_visibility_levels do |record, attr, value|
value&.each do |level| value&.each do |level|
unless Gitlab::VisibilityLevel.options.has_value?(level) unless Gitlab::VisibilityLevel.options.has_value?(level)
...@@ -233,7 +237,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -233,7 +237,8 @@ class ApplicationSetting < ActiveRecord::Base
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0, terminal_max_session_time: 0,
two_factor_grace_period: 48, two_factor_grace_period: 48,
user_default_external: false user_default_external: false,
polling_interval_multiplier: 1
} }
end end
......
...@@ -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)")
......
...@@ -3,4 +3,7 @@ module Importable ...@@ -3,4 +3,7 @@ module Importable
attr_accessor :importing attr_accessor :importing
alias_method :importing?, :importing alias_method :importing?, :importing
attr_accessor :imported
alias_method :imported?, :imported
end end
...@@ -14,6 +14,7 @@ module Issuable ...@@ -14,6 +14,7 @@ module Issuable
include Awardable include Awardable
include Taskable include Taskable
include TimeTrackable include TimeTrackable
include Importable
# This object is used to gather issuable meta data for displaying # This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests # upvotes, downvotes, notes and closing merge requests count for issues and merge requests
...@@ -99,7 +100,7 @@ module Issuable ...@@ -99,7 +100,7 @@ module Issuable
acts_as_paranoid acts_as_paranoid
after_save :update_assignee_cache_counts, if: :assignee_id_changed? after_save :update_assignee_cache_counts, if: :assignee_id_changed?
after_save :record_metrics after_save :record_metrics, unless: :imported?
def update_assignee_cache_counts def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist) # make sure we flush the cache for both the old *and* new assignees(if they exist)
......
module RepositoryMirroring
def set_remote_as_mirror(name)
config = raw_repository.rugged.config
# This is used to define repository as equivalent as "git clone --mirror"
config["remote.#{name}.fetch"] = 'refs/*:refs/*'
config["remote.#{name}.mirror"] = true
config["remote.#{name}.prune"] = true
end
def fetch_mirror(remote, url)
add_remote(remote, url)
set_remote_as_mirror(remote)
fetch_remote(remote, forced: true)
remove_remote(remote)
end
end
...@@ -3,7 +3,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -3,7 +3,6 @@ class MergeRequest < ActiveRecord::Base
include Issuable include Issuable
include Referable include Referable
include Sortable include Sortable
include Importable
belongs_to :target_project, class_name: "Project" belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project" belongs_to :source_project, class_name: "Project"
......
...@@ -30,7 +30,7 @@ class Milestone < ActiveRecord::Base ...@@ -30,7 +30,7 @@ class Milestone < ActiveRecord::Base
validates :title, presence: true, uniqueness: { scope: :project_id } validates :title, presence: true, uniqueness: { scope: :project_id }
validates :project, presence: true validates :project, presence: true
validate :start_date_should_be_less_than_due_date, if: Proc.new { |m| m.start_date.present? && m.due_date.present? } validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
strip_attributes :title strip_attributes :title
......
...@@ -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
...@@ -535,6 +535,10 @@ class Project < ActiveRecord::Base ...@@ -535,6 +535,10 @@ class Project < ActiveRecord::Base
import_type == 'gitea' import_type == 'gitea'
end end
def github_import?
import_type == 'github'
end
def check_limit def check_limit
unless creator.can_create_project? || namespace.kind == 'group' unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit projects_limit = creator.projects_limit
......
...@@ -2,6 +2,7 @@ require 'securerandom' ...@@ -2,6 +2,7 @@ require 'securerandom'
class Repository class Repository
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include RepositoryMirroring
attr_accessor :path_with_namespace, :project attr_accessor :path_with_namespace, :project
...@@ -64,7 +65,7 @@ class Repository ...@@ -64,7 +65,7 @@ class Repository
# Return absolute path to repository # Return absolute path to repository
def path_to_repo def path_to_repo
@path_to_repo ||= File.expand_path( @path_to_repo ||= File.expand_path(
File.join(@project.repository_storage_path, path_with_namespace + ".git") File.join(repository_storage_path, path_with_namespace + ".git")
) )
end end
...@@ -401,10 +402,6 @@ class Repository ...@@ -401,10 +402,6 @@ class Repository
expire_tags_cache expire_tags_cache
end end
def before_import
expire_content_cache
end
# Runs code after the HEAD of a repository is changed. # Runs code after the HEAD of a repository is changed.
def after_change_head def after_change_head
expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys) expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
...@@ -1033,6 +1030,23 @@ class Repository ...@@ -1033,6 +1030,23 @@ class Repository
rugged.references.delete(tmp_ref) if tmp_ref rugged.references.delete(tmp_ref) if tmp_ref
end end
def add_remote(name, url)
raw_repository.remote_add(name, url)
rescue Rugged::ConfigError
raw_repository.remote_update(name, url: url)
end
def remove_remote(name)
raw_repository.remote_delete(name)
true
rescue Rugged::ConfigError
false
end
def fetch_remote(remote, forced: false, no_tags: false)
gitlab_shell.fetch_remote(repository_storage_path, path_with_namespace, remote, forced: forced, no_tags: no_tags)
end
def fetch_ref(source_path, source_ref, target_ref) def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo) Gitlab::Popen.popen(args, path_to_repo)
...@@ -1150,4 +1164,8 @@ class Repository ...@@ -1150,4 +1164,8 @@ class Repository
def repository_event(event, tags = {}) def repository_event(event, tags = {})
Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags)) Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
end end
def repository_storage_path
@project.repository_storage_path
end
end end
...@@ -25,7 +25,7 @@ class Service < ActiveRecord::Base ...@@ -25,7 +25,7 @@ class Service < ActiveRecord::Base
belongs_to :project, inverse_of: :services belongs_to :project, inverse_of: :services
has_one :service_hook has_one :service_hook
validates :project_id, presence: true, unless: Proc.new { |service| service.template? } validates :project_id, presence: true, unless: proc { |service| service.template? }
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') } scope :issue_trackers, -> { where(category: 'issue_tracker') }
......
...@@ -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
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
# #
class NotificationRecipientService class NotificationRecipientService
attr_reader :project attr_reader :project
def initialize(project) def initialize(project)
@project = project @project = project
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)
recipients = add_project_watchers(recipients)
unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
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
...@@ -278,11 +278,11 @@ class NotificationService ...@@ -278,11 +278,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
......
...@@ -11,7 +11,7 @@ module Projects ...@@ -11,7 +11,7 @@ module Projects
success success
rescue => e rescue => e
error(e.message) error("Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}")
end end
private private
...@@ -32,23 +32,40 @@ module Projects ...@@ -32,23 +32,40 @@ module Projects
end end
def import_repository def import_repository
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
begin begin
raise Error, "Blocked import URL." if Gitlab::UrlBlocker.blocked_url?(project.import_url) if project.github_import? || project.gitea_import?
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url) fetch_repository
rescue => e else
clone_repository
end
rescue Gitlab::Shell::Error => e
# Expire cache to prevent scenarios such as: # Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true # 2. Retried import, repo is broken or not imported but +exists?+ still returns true
project.repository.before_import if project.repository_exists? project.repository.expire_content_cache if project.repository_exists?
raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" raise Error, e.message
end end
end end
def clone_repository
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
end
def fetch_repository
project.create_repository
project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true)
project.repository.remove_remote(project.import_type)
end
def import_data def import_data
return unless has_importer? return unless has_importer?
project.repository.before_import unless project.gitlab_project_import? project.repository.expire_content_cache unless project.gitlab_project_import?
unless importer.execute unless importer.execute
raise Error, 'The remote data could not be imported.' raise Error, 'The remote data could not be imported.'
......
...@@ -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
......
...@@ -204,7 +204,7 @@ class TodoService ...@@ -204,7 +204,7 @@ class TodoService
# Only update those that are not really on that state # Only update those that are not really on that state
todos = todos.where.not(state: state) todos = todos.where.not(state: state)
todos_ids = todos.pluck(:id) todos_ids = todos.pluck(:id)
todos.update_all(state: state) todos.unscope(:order).update_all(state: state)
current_user.update_todos_count_cache current_user.update_todos_count_cache
todos_ids todos_ids
end end
......
...@@ -20,10 +20,10 @@ module Users ...@@ -20,10 +20,10 @@ module Users
Groups::DestroyService.new(group, current_user).execute Groups::DestroyService.new(group, current_user).execute
end end
user.personal_projects.each do |project| user.personal_projects.with_deleted.each do |project|
# Skip repository removal because we remove directory with namespace # Skip repository removal because we remove directory with namespace
# that contain all this repositories # that contain all this repositories
::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end end
move_issues_to_ghost_user(user) move_issues_to_ghost_user(user)
......
...@@ -558,5 +558,19 @@ ...@@ -558,5 +558,19 @@
Maximum time for web terminal websocket connection (in seconds). Maximum time for web terminal websocket connection (in seconds).
0 for unlimited. 0 for unlimited.
%fieldset
%legend Real-time features
.form-group
= f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :polling_interval_multiplier, class: 'form-control'
.help-block
Change this value to influence how frequently the GitLab UI polls for updates.
If you set the value to 2 all polling intervals are multiplied
by 2, which means that polling happens half as frequently.
The multiplier can also have a decimal value.
The default value (1) is a reasonable choice for the majority of GitLab
installations. Set to 0 to completely disable polling.
.form-actions .form-actions
= f.submit 'Save', class: 'btn btn-save' = f.submit 'Save', class: 'btn btn-save'
- 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
......
...@@ -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")
class RepositoryImportWorker class RepositoryImportWorker
include Sidekiq::Worker include Sidekiq::Worker
include Gitlab::ShellAdapter
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
attr_accessor :project, :current_user attr_accessor :project, :current_user
......
---
title: Fix symlink icon in project tree
merge_request: 9780
author: mhasbini
---
title: Remove no-new annotation from file_template_mediator.js.
merge_request: !9782
author:
---
title: Fix GitHub Importer for PRs of deleted forked repositories
merge_request: 9992
author:
---
title: Fix redirection after login when the referer have params
merge_request:
author: mhasbini
---
title: Fixes method not replacing URL parameters correctly and breaking pipelines
pagination
merge_request:
author:
---
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: Improve performance of GitHub importer for large repositories.
merge_request: 10273
author:
---
title: Introduce "polling_interval_multiplier" as application setting
merge_request: 10280
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: Fix race condition where a namespace would be deleted before a project was
deleted
merge_request:
author:
---
title: Relax constraint on Wiki IDs, since subdirectories can contain spaces
merge_request:
author:
---
title: Enable Style/Proc cop for rubocop
merge_request:
author: mhasbini
---
title: Remove unnecessary ORDER BY clause when updating todos
merge_request:
author: mhasbini
...@@ -461,7 +461,7 @@ production: &base ...@@ -461,7 +461,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
WIKI_SLUG_ID = { id: /\S+/ }.freeze unless defined? WIKI_SLUG_ID
scope(controller: :wikis) do scope(controller: :wikis) do
scope(path: 'wikis', as: :wikis) do scope(path: 'wikis', as: :wikis) do
get :git_access get :git_access
...@@ -8,7 +6,7 @@ scope(controller: :wikis) do ...@@ -8,7 +6,7 @@ scope(controller: :wikis) do
post '/', to: 'wikis#create' post '/', to: 'wikis#create'
end end
scope(path: 'wikis/*id', as: :wiki, constraints: WIKI_SLUG_ID, format: false) do scope(path: 'wikis/*id', as: :wiki, format: false) do
get :edit get :edit
get :history get :history
post :preview_markdown post :preview_markdown
......
require 'factory_girl_rails' module Db
module Fixtures
module Development
class AbuseReport
def self.seed
Gitlab::Seeder.quiet do
(::AbuseReport.default_per_page + 3).times do |i|
reported_user =
::User.create!(
username: "reported_user_#{i}",
name: FFaker::Name.name,
email: FFaker::Internet.email,
confirmed_at: DateTime.now,
password: '12345678'
)
(AbuseReport.default_per_page + 3).times do ::AbuseReport.create(reporter: ::User.take, user: reported_user, message: 'User sends spam')
FactoryGirl.create(:abuse_report) print '.'
end
end
end
end
end
end
end end
Db::Fixtures::Development::AbuseReport.seed
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddPollingIntervalMultiplierToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
disable_ddl_transaction!
def up
add_column_with_default :application_settings, :polling_interval_multiplier, :decimal, default: 1, allow_null: false
end
def down
remove_column :application_settings, :polling_interval_multiplier
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: 20170322013926) do ActiveRecord::Schema.define(version: 20170329124448) 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"
...@@ -115,6 +115,7 @@ ActiveRecord::Schema.define(version: 20170322013926) do ...@@ -115,6 +115,7 @@ ActiveRecord::Schema.define(version: 20170322013926) do
t.integer "unique_ips_limit_per_user" t.integer "unique_ips_limit_per_user"
t.integer "unique_ips_limit_time_window" t.integer "unique_ips_limit_time_window"
t.boolean "unique_ips_limit_enabled", default: false, null: false t.boolean "unique_ips_limit_enabled", default: false, null: false
t.decimal "polling_interval_multiplier", default: 1.0, null: false
end end
create_table "audit_events", force: :cascade do |t| create_table "audit_events", force: :cascade do |t|
......
...@@ -48,7 +48,8 @@ Example response: ...@@ -48,7 +48,8 @@ Example response:
"koding_url": null, "koding_url": null,
"plantuml_enabled": false, "plantuml_enabled": false,
"plantuml_url": null, "plantuml_url": null,
"terminal_max_session_time": 0 "terminal_max_session_time": 0,
"polling_interval_multiplier": 1.0
} }
``` ```
...@@ -88,6 +89,7 @@ PUT /application/settings ...@@ -88,6 +89,7 @@ PUT /application/settings
| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. | | `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. |
| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. | | `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. |
| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. | | `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. |
| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling. |
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
...@@ -124,6 +126,7 @@ Example response: ...@@ -124,6 +126,7 @@ Example response:
"koding_url": null, "koding_url": null,
"plantuml_enabled": false, "plantuml_enabled": false,
"plantuml_url": null, "plantuml_url": null,
"terminal_max_session_time": 0 "terminal_max_session_time": 0,
"polling_interval_multiplier": 1.0
} }
``` ```
...@@ -352,7 +352,7 @@ Example values: ...@@ -352,7 +352,7 @@ Example values:
export CI_JOB_ID="50" export CI_JOB_ID="50"
export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a" export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a"
export CI_COMMIT_REF_NAME="master" export CI_COMMIT_REF_NAME="master"
export CI_REPOSITORY_URL="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git" export CI_REPOSITORY_URL="https://gitlab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git"
export CI_COMMIT_TAG="1.0.0" export CI_COMMIT_TAG="1.0.0"
export CI_JOB_NAME="spec:other" export CI_JOB_NAME="spec:other"
export CI_JOB_STAGE="test" export CI_JOB_STAGE="test"
......
...@@ -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
...@@ -89,8 +88,8 @@ In all of the below cases, the notification will be sent to: ...@@ -89,8 +88,8 @@ In all of the below cases, the notification will be sent to:
| Reopen merge request | | | Reopen merge request | |
| Merge merge request | | | Merge 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
......
...@@ -3,6 +3,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps ...@@ -3,6 +3,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
include SharedPaths include SharedPaths
include SharedProject include SharedProject
include SharedUser include SharedUser
include WaitForAjax
step '"John Doe" is a developer of project "Shop"' do step '"John Doe" is a developer of project "Shop"' do
project.team << [john_doe, :developer] project.team << [john_doe, :developer]
...@@ -138,6 +139,8 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps ...@@ -138,6 +139,8 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
step 'I should be directed to the corresponding page' do step 'I should be directed to the corresponding page' do
page.should have_css('.identifier', text: 'Merge Request !1') page.should have_css('.identifier', text: 'Merge Request !1')
# Merge request page loads and issues a number of Ajax requests
wait_for_ajax
end end
def should_see_todo(position, title, body, state: :pending) def should_see_todo(position, title, body, state: :pending)
......
...@@ -23,13 +23,13 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps ...@@ -23,13 +23,13 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
end end
step 'I submit new hook' do step 'I submit new hook' do
@url = FFaker::Internet.uri("http") @url = 'http://example.org/1'
fill_in "hook_url", with: @url fill_in "hook_url", with: @url
expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1) expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1)
end end
step 'I submit new hook with SSL verification enabled' do step 'I submit new hook with SSL verification enabled' do
@url = FFaker::Internet.uri("http") @url = 'http://example.org/2'
fill_in "hook_url", with: @url fill_in "hook_url", with: @url
check "hook_enable_ssl_verification" check "hook_enable_ssl_verification"
expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1) expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1)
......
...@@ -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
......
...@@ -121,7 +121,7 @@ module API ...@@ -121,7 +121,7 @@ module API
end end
def oauth2_bearer_token_error_handler def oauth2_bearer_token_error_handler
Proc.new do |e| proc do |e|
response = response =
case e case e
when MissingTokenError when MissingTokenError
......
...@@ -204,7 +204,7 @@ module API ...@@ -204,7 +204,7 @@ module API
expose :id, :name, :type, :path expose :id, :name, :type, :path
expose :mode do |obj, options| expose :mode do |obj, options|
filemode = obj.mode.to_s(8) filemode = obj.mode
filemode = "0" + filemode if filemode.length < 6 filemode = "0" + filemode if filemode.length < 6
filemode filemode
end end
...@@ -581,6 +581,7 @@ module API ...@@ -581,6 +581,7 @@ module API
expose :plantuml_enabled expose :plantuml_enabled
expose :plantuml_url expose :plantuml_url
expose :terminal_max_session_time expose :terminal_max_session_time
expose :polling_interval_multiplier
end end
class Release < Grape::Entity class Release < Grape::Entity
......
...@@ -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?
......
...@@ -110,6 +110,7 @@ module API ...@@ -110,6 +110,7 @@ module API
requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run." requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run."
end end
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility, at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility,
:default_group_visibility, :restricted_visibility_levels, :import_sources, :default_group_visibility, :restricted_visibility_levels, :import_sources,
:enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit, :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
...@@ -125,7 +126,7 @@ module API ...@@ -125,7 +126,7 @@ module API
:akismet_enabled, :admin_notification_email, :sentry_enabled, :akismet_enabled, :admin_notification_email, :sentry_enabled,
:repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
:version_check_enabled, :email_author_in_body, :html_emails_enabled, :version_check_enabled, :email_author_in_body, :html_emails_enabled,
:housekeeping_enabled, :terminal_max_session_time :housekeeping_enabled, :terminal_max_session_time, :polling_interval_multiplier
end end
put "application/settings" do put "application/settings" do
attrs = declared_params(include_missing: false) attrs = declared_params(include_missing: false)
......
...@@ -293,7 +293,7 @@ module API ...@@ -293,7 +293,7 @@ module API
user = User.find_by(id: params[:id]) user = User.find_by(id: params[:id])
not_found!('User') unless user not_found!('User') unless user
::Users::DestroyService.new(current_user).execute(user) DeleteUserWorker.perform_async(current_user.id, user.id)
end end
desc 'Block a user. Available only for admins.' desc 'Block a user. Available only for admins.'
......
...@@ -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)
......
...@@ -18,8 +18,7 @@ module Gitlab ...@@ -18,8 +18,7 @@ module Gitlab
if_none_match = env['HTTP_IF_NONE_MATCH'] if_none_match = env['HTTP_IF_NONE_MATCH']
if if_none_match == etag if if_none_match == etag
Gitlab::Metrics.add_event(:etag_caching_cache_hit) handle_cache_hit(etag)
[304, { 'ETag' => etag }, ['']]
else else
track_cache_miss(if_none_match, cached_value_present) track_cache_miss(if_none_match, cached_value_present)
...@@ -52,6 +51,14 @@ module Gitlab ...@@ -52,6 +51,14 @@ module Gitlab
%Q{W/"#{value}"} %Q{W/"#{value}"}
end end
def handle_cache_hit(etag)
Gitlab::Metrics.add_event(:etag_caching_cache_hit)
status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429
[status_code, { 'ETag' => etag }, ['']]
end
def track_cache_miss(if_none_match, cached_value_present) def track_cache_miss(if_none_match, cached_value_present)
if if_none_match.blank? if if_none_match.blank?
Gitlab::Metrics.add_event(:etag_caching_header_missing) Gitlab::Metrics.add_event(:etag_caching_header_missing)
......
...@@ -33,7 +33,7 @@ module Gitlab ...@@ -33,7 +33,7 @@ module Gitlab
root_id: root_tree.oid, root_id: root_tree.oid,
name: entry[:name], name: entry[:name],
type: entry[:type], type: entry[:type],
mode: entry[:filemode], mode: entry[:filemode].to_s(8),
path: path ? File.join(path, entry[:name]) : entry[:name], path: path ? File.join(path, entry[:name]) : entry[:name],
commit_id: sha, commit_id: sha,
) )
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment