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:
Style/WordArray:
Enabled: true
# Use `proc` instead of `Proc.new`.
Style/Proc:
Enabled: true
# Metrics #####################################################################
# A calculated magnitude based on number of assignments,
......
......@@ -226,11 +226,6 @@ Style/PredicateName:
Style/PreferredHashMethods:
Enabled: false
# Offense count: 8
# Cop supports --auto-correct.
Style/Proc:
Enabled: false
# Offense count: 62
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
......
......@@ -260,7 +260,6 @@ group :development do
gem 'brakeman', '~> 3.6.0', require: false
gem 'letter_opener_web', '~> 1.3.0'
gem 'bullet', '~> 5.5.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
# Better errors handler
......@@ -272,6 +271,7 @@ group :development do
end
group :development, :test do
gem 'bullet', '~> 5.5.0', require: !!ENV['ENABLE_BULLET']
gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'pry-rails', '~> 0.3.4'
......@@ -352,4 +352,4 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
gem 'gitaly', '~> 0.3.0'
gem 'gitaly', '~> 0.5.0'
......@@ -253,7 +253,7 @@ GEM
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
gitaly (0.3.0)
gitaly (0.5.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -899,7 +899,7 @@ DEPENDENCIES
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0)
gitaly (~> 0.3.0)
gitaly (~> 0.5.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.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 @@ $(() => {
const urlRoot = editBlobForm.data('relative-url-root');
const assetsPath = editBlobForm.data('assets-prefix');
const blobLanguage = editBlobForm.data('blob-language');
const currentAction = $('.js-file-title').data('current-action');
new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage);
new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction);
new NewCommitForm(editBlobForm);
}
......
/* global ace */
import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors';
import BlobGitignoreSelectors from '../blob/template_selectors/blob_gitignore_selectors';
import BlobCiYamlSelectors from '../blob/template_selectors/blob_ci_yaml_selectors';
import BlobDockerfileSelectors from '../blob/template_selectors/blob_dockerfile_selectors';
import TemplateSelectorMediator from '../blob/file_template_mediator';
export default class EditBlob {
constructor(assetsPath, aceMode) {
constructor(assetsPath, aceMode, currentAction) {
this.configureAceEditor(aceMode, assetsPath);
this.prepFileContentForSubmit();
this.initModePanesAndLinks();
this.initSoftWrap();
this.initFileSelectors();
this.initFileSelectors(currentAction);
}
configureAceEditor(aceMode, assetsPath) {
......@@ -19,6 +15,10 @@ export default class EditBlob {
ace.config.loadModule('ace/ext/searchbox');
this.editor = ace.edit('editor');
// This prevents warnings re: automatic scrolling being logged
this.editor.$blockScrolling = Infinity;
this.editor.focus();
if (aceMode) {
......@@ -26,29 +26,13 @@ export default class EditBlob {
}
}
prepFileContentForSubmit() {
$('form').submit(() => {
$('#file-content').val(this.editor.getValue());
initFileSelectors(currentAction) {
this.fileTemplateMediator = new TemplateSelectorMediator({
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() {
this.$editModePanes = $('.js-edit-mode-pane');
this.$editModeLinks = $('.js-edit-mode a');
......
......@@ -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 param for page is already present, we'll update it
......@@ -278,17 +278,24 @@
let search;
const locationSearch = window.location.search;
if (locationSearch.length === 0) {
search = `?${param}=${value}`;
}
if (locationSearch.length) {
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) {
const regex = new RegExp(param + '=\\d');
search = locationSearch.replace(regex, `${param}=${value}`);
}
parameters[param] = value;
if (locationSearch.length && locationSearch.indexOf(param) === -1) {
search = `${locationSearch}&${param}=${value}`;
const toString = Object.keys(parameters)
.map(val => `${val}=${encodeURIComponent(parameters[val])}`)
.join('&');
search = `?${toString}`;
} else {
search = `?${param}=${value}`;
}
return search;
......
/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */
import TemplateSelector from '../blob/template_selectors/template_selector';
import TemplateSelector from '../blob/template_selector';
((global) => {
class IssuableTemplateSelector extends TemplateSelector {
......
.file-editor {
.nav-links {
border-top: 1px solid $border-color;
border-right: 1px solid $border-color;
border-left: 1px solid $border-color;
border-bottom: none;
border-radius: 2px;
background: $gray-normal;
}
#editor {
border: none;
border-radius: 0;
......@@ -72,11 +81,7 @@
}
.encoding-selector,
.soft-wrap-toggle,
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector {
.soft-wrap-toggle {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
......@@ -103,28 +108,9 @@
}
}
}
.gitignore-selector,
.license-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector {
.dropdown {
line-height: 21px;
}
.dropdown-menu-toggle {
vertical-align: top;
width: 220px;
}
}
.gitlab-ci-yml-selector {
.dropdown-menu-toggle {
width: 250px;
}
}
}
@media(max-width: $screen-xs-max){
.file-editor {
.file-title {
......@@ -149,10 +135,7 @@
margin: 3px 0;
}
.encoding-selector,
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector {
.encoding-selector {
display: block;
margin: 3px 0;
......@@ -163,3 +146,104 @@
}
}
}
.blob-new-page-title,
.blob-edit-page-title {
margin: 19px 0 21px;
vertical-align: top;
display: inline-block;
@media(max-width: $screen-sm-max) {
display: block;
margin: 19px 0 12px;
}
}
.template-selectors-menu {
display: inline-block;
vertical-align: top;
margin: 14px 0 0 16px;
padding: 0 0 0 14px;
border-left: 1px solid $border-color;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 5px 0;
padding: 0;
border-left: none;
}
}
.templates-selectors-label {
display: inline-block;
vertical-align: top;
margin-top: 6px;
line-height: 21px;
@media(max-width: $screen-sm-max) {
display: block;
margin: 5px 0;
}
}
.template-selector-dropdowns-wrap {
display: inline-block;
margin-left: 8px;
vertical-align: top;
margin: 5px 0 0 8px;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 0 0 16px;
}
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector,
.template-type-selector {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
margin-top: -5px;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 5px 0;
}
.dropdown {
line-height: 21px;
}
.dropdown-menu-toggle {
width: 250px;
vertical-align: top;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 5px 0;
}
}
}
}
.template-selectors-undo-menu {
display: inline-block;
margin: 7px 0 0 10px;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 20px 0;
}
button {
margin: -4px 0 0 15px;
}
}
class Admin::AbuseReportsController < Admin::ApplicationController
def index
@abuse_reports = AbuseReport.order(id: :desc).page(params[:page])
@abuse_reports.includes(:reporter, :user)
end
def destroy
......
......@@ -134,6 +134,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:unique_ips_limit_enabled,
:version_check_enabled,
:terminal_max_session_time,
:polling_interval_multiplier,
disabled_oauth_sign_in_sources: [],
import_sources: [],
......
......@@ -14,6 +14,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort)
@members = @members.page(params[:page]).per(50)
@members.includes(:user)
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
......
......@@ -57,7 +57,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
def render_ok
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.git_http_ok(repository, user)
render json: Gitlab::Workhorse.git_http_ok(repository, user, action_name)
end
def render_http_not_allowed
......
......@@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@collection_type = "MergeRequest"
@merge_requests = merge_requests_collection
@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)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
......
......@@ -21,9 +21,9 @@ class Projects::MilestonesController < Projects::ApplicationController
@sort = params[:sort] || 'due_date_asc'
@milestones = @milestones.sort(@sort)
@milestones = @milestones.includes(:project)
respond_to do |format|
format.html do
@milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page])
end
format.json do
......
......@@ -25,12 +25,12 @@ class RegistrationsController < Devise::RegistrationsController
end
def destroy
Users::DestroyService.new(current_user).execute(current_user)
DeleteUserWorker.perform_async(current_user.id, current_user.id)
respond_to do |format|
format.html do
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
......
......@@ -79,7 +79,7 @@ class SessionsController < Devise::SessionsController
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
referer_uri = URI(request.referer)
if referer_uri.host == Gitlab.config.gitlab.host
referer_uri.path
referer_uri.request_uri
else
request.fullpath
end
......
......@@ -5,8 +5,8 @@ class BaseMailer < ActionMailer::Base
attr_accessor :current_user
helper_method :current_user, :can?
default from: Proc.new { default_sender_address.format }
default reply_to: Proc.new { default_reply_to_address.format }
default from: proc { default_sender_address.format }
default reply_to: proc { default_reply_to_address.format }
def can?
Ability.allowed?(current_user, action, subject)
......
......@@ -131,6 +131,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
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|
value&.each do |level|
unless Gitlab::VisibilityLevel.options.has_value?(level)
......@@ -233,7 +237,8 @@ class ApplicationSetting < ActiveRecord::Base
signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0,
two_factor_grace_period: 48,
user_default_external: false
user_default_external: false,
polling_interval_multiplier: 1
}
end
......
......@@ -164,11 +164,6 @@ module Ci
builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end
# For now the only user who participates is the user who triggered
def participants(_current_user = nil)
Array(user)
end
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
......
......@@ -3,4 +3,7 @@ module Importable
attr_accessor :importing
alias_method :importing?, :importing
attr_accessor :imported
alias_method :imported?, :imported
end
......@@ -14,6 +14,7 @@ module Issuable
include Awardable
include Taskable
include TimeTrackable
include Importable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
......@@ -99,7 +100,7 @@ module Issuable
acts_as_paranoid
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
# 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
include Issuable
include Referable
include Sortable
include Importable
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
......
......@@ -30,7 +30,7 @@ class Milestone < ActiveRecord::Base
validates :title, presence: true, uniqueness: { scope: :project_id }
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
......
......@@ -60,16 +60,25 @@ class NotificationSetting < ActiveRecord::Base
def set_events
return if custom?
EMAIL_EVENTS.each do |event|
events[event] = false
end
self.events = {}
end
# Validates store accessors values as boolean
# It is a text field so it does not cast correct boolean values in JSON
def events_to_boolean
EMAIL_EVENTS.each do |event|
events[event] = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(events[event])
bool = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(public_send(event))
events[event] = bool
end
end
# Allow people to receive failed pipeline notifications if they already have
# custom notifications enabled, as these are more like mentions than the other
# custom settings.
def failed_pipeline
bool = super
bool.nil? || bool
end
end
......@@ -535,6 +535,10 @@ class Project < ActiveRecord::Base
import_type == 'gitea'
end
def github_import?
import_type == 'github'
end
def check_limit
unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit
......
......@@ -2,6 +2,7 @@ require 'securerandom'
class Repository
include Gitlab::ShellAdapter
include RepositoryMirroring
attr_accessor :path_with_namespace, :project
......@@ -64,7 +65,7 @@ class Repository
# Return absolute path to repository
def path_to_repo
@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
......@@ -401,10 +402,6 @@ class Repository
expire_tags_cache
end
def before_import
expire_content_cache
end
# Runs code after the HEAD of a repository is changed.
def after_change_head
expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
......@@ -1033,6 +1030,23 @@ class Repository
rugged.references.delete(tmp_ref) if tmp_ref
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)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
......@@ -1150,4 +1164,8 @@ class Repository
def repository_event(event, tags = {})
Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
end
def repository_storage_path
@project.repository_storage_path
end
end
......@@ -25,7 +25,7 @@ class Service < ActiveRecord::Base
belongs_to :project, inverse_of: :services
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 :issue_trackers, -> { where(category: 'issue_tracker') }
......
......@@ -24,6 +24,10 @@ class BaseService
Gitlab::AppLogger.info message
end
def log_error(message)
Gitlab::AppLogger.error message
end
def system_hook_service
SystemHooksService.new
end
......
......@@ -5,8 +5,6 @@ module Ci
def execute(pipeline)
@pipeline = pipeline
ensure_created_builds! # TODO, remove me in 9.0
new_builds =
stage_indexes_of_created_builds.map do |index|
process_stage(index)
......@@ -73,18 +71,5 @@ module Ci
def created_builds
pipeline.builds.created
end
# This method is DEPRECATED and should be removed in 9.0.
#
# We need it to maintain backwards compatibility with previous versions
# when builds were not created within one transaction with the pipeline.
#
def ensure_created_builds!
return if created_builds.any?
Ci::CreatePipelineBuildsService
.new(project, current_user)
.execute(pipeline)
end
end
end
......@@ -3,7 +3,7 @@
#
class NotificationRecipientService
attr_reader :project
def initialize(project)
@project = project
end
......@@ -12,11 +12,7 @@ class NotificationRecipientService
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
recipients = add_project_watchers(recipients)
end
recipients = add_project_watchers(recipients)
recipients = add_custom_notifications(recipients, custom_action)
recipients = reject_mention_users(recipients)
......@@ -43,6 +39,28 @@ class NotificationRecipientService
recipients.uniq
end
def build_pipeline_recipients(target, current_user, action:)
return [] unless current_user
custom_action =
case action.to_s
when 'failed'
:failed_pipeline
when 'success'
:success_pipeline
end
notification_setting = notification_setting_for_user_project(current_user, target.project)
return [] if notification_setting.mention? || notification_setting.disabled?
return [] if notification_setting.custom? && !notification_setting.public_send(custom_action)
return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
reject_users_without_access([current_user], target)
end
def build_relabeled_recipients(target, current_user, labels:)
recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
......@@ -290,4 +308,16 @@ class NotificationRecipientService
def build_custom_key(action, object)
"#{action}_#{object.class.model_name.name.underscore}".to_sym
end
def notification_setting_for_user_project(user, project)
project_setting = user.notification_settings_for(project)
return project_setting unless project_setting.global?
group_setting = user.notification_settings_for(project.group)
return group_setting unless group_setting.global?
user.global_notification_setting
end
end
......@@ -278,11 +278,11 @@ class NotificationService
return unless mailer.respond_to?(email_template)
recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients(
recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients(
pipeline,
pipeline.user,
action: pipeline.status,
skip_current_user: false).map(&:notification_email)
).map(&:notification_email)
if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later
......
......@@ -11,7 +11,7 @@ module Projects
success
rescue => e
error(e.message)
error("Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}")
end
private
......@@ -32,23 +32,40 @@ module Projects
end
def import_repository
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
begin
raise Error, "Blocked import URL." if Gitlab::UrlBlocker.blocked_url?(project.import_url)
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
rescue => e
if project.github_import? || project.gitea_import?
fetch_repository
else
clone_repository
end
rescue Gitlab::Shell::Error => e
# Expire cache to prevent scenarios such as:
# 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
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
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
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
raise Error, 'The remote data could not be imported.'
......
......@@ -46,6 +46,7 @@ module Projects
end
def error(message, http_status = nil)
log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest?
@status.description = message
@status.drop
......
......@@ -204,7 +204,7 @@ class TodoService
# Only update those that are not really on that state
todos = todos.where.not(state: state)
todos_ids = todos.pluck(:id)
todos.update_all(state: state)
todos.unscope(:order).update_all(state: state)
current_user.update_todos_count_cache
todos_ids
end
......
......@@ -20,10 +20,10 @@ module Users
Groups::DestroyService.new(group, current_user).execute
end
user.personal_projects.each do |project|
user.personal_projects.with_deleted.each do |project|
# Skip repository removal because we remove directory with namespace
# 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
move_issues_to_ghost_user(user)
......
......@@ -558,5 +558,19 @@
Maximum time for web terminal websocket connection (in seconds).
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
= f.submit 'Save', class: 'btn btn-save'
- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create'
.file-holder.file.append-bottom-default
.js-file-title.file-title.clearfix
.js-file-title.file-title.clearfix{ data: { current_action: action } }
.editor-ref
= icon('code-fork')
= ref
%span.editor-file-name
- if current_action?(:edit) || current_action?(:update)
= text_field_tag 'file_path', (params[:file_path] || @path),
class: 'form-control new-file-path'
class: 'form-control new-file-path js-file-path-name-input'
- if current_action?(:new) || current_action?(:create)
%span.editor-file-name
\/
= text_field_tag 'file_name', params[:file_name], placeholder: "File name",
required: true, class: 'form-control new-file-name'
required: true, class: 'form-control new-file-name js-file-path-name-input'
.pull-right.file-buttons
.license-selector.js-license-selector-wrap.hidden
= dropdown_tag("Choose a License template", options: { toggle_class: 'btn js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.hidden
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
= dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.dockerfile-selector.js-dockerfile-selector-wrap.hidden
= dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
= button_tag class: 'soft-wrap-toggle btn', type: 'button' do
= button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do
%span.no-wrap
= custom_icon('icon_no_wrap')
No wrap
......@@ -31,7 +25,7 @@
= custom_icon('icon_soft_wrap')
Soft wrap
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1'
.file-editor.code
%pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data]
......
.template-selectors-menu
.templates-selectors-label
Template
.template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden
= dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } )
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a License template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
.template-selectors-undo-menu.hidden
%span.text-info Template applied
%button.btn.btn-sm.btn-info Undo
......@@ -11,12 +11,15 @@
Someone edited the file the same time you did. Please check out
= link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs.
.editor-title-row
%h3.page-title.blob-edit-page-title
Edit file
= render 'template_selectors'
.file-editor
%ul.nav-links.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
Edit File
Write
%li
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
......
......@@ -2,10 +2,10 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
= page_specific_javascript_bundle_tag('blob')
%h3.page-title
New File
.editor-title-row
%h3.page-title.blob-new-page-title
New file
= render 'template_selectors'
.file-editor
= form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref
......
......@@ -25,7 +25,7 @@
.form-group
.checkbox{ class: ("prepend-top-0" if index == 0) }
%label{ for: field_id }
= check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
= check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.public_send(event))
%strong
= notification_event_name(event)
= icon("spinner spin", class: "custom-notification-event-loading")
class RepositoryImportWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
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
storages: # You must have at least a `default` storage path.
default:
path: /home/git/repositories/
gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket
gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket # TCP connections are supported too (e.g. tcp://host:port)
## Backup settings
backup:
......
......@@ -9,7 +9,7 @@ if Gitlab.config.gitaly.enabled || Rails.env.test?
raise "storage #{name.inspect} is missing a gitaly_address"
end
unless URI(address).scheme == 'unix'
unless URI(address).scheme.in?(%w(tcp unix))
raise "Unsupported Gitaly address: #{address.inspect}"
end
......
if ENV['ENABLE_BULLET']
require 'bullet'
if defined?(Bullet) && ENV['ENABLE_BULLET']
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.enable = true
Bullet.console = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.raise = Rails.env.test?
end
end
end
WIKI_SLUG_ID = { id: /\S+/ }.freeze unless defined? WIKI_SLUG_ID
scope(controller: :wikis) do
scope(path: 'wikis', as: :wikis) do
get :git_access
......@@ -8,7 +6,7 @@ scope(controller: :wikis) do
post '/', to: 'wikis#create'
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 :history
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
FactoryGirl.create(:abuse_report)
::AbuseReport.create(reporter: ::User.take, user: reported_user, message: 'User sends spam')
print '.'
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 @@
#
# 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
enable_extension "plpgsql"
......@@ -115,6 +115,7 @@ ActiveRecord::Schema.define(version: 20170322013926) do
t.integer "unique_ips_limit_per_user"
t.integer "unique_ips_limit_time_window"
t.boolean "unique_ips_limit_enabled", default: false, null: false
t.decimal "polling_interval_multiplier", default: 1.0, null: false
end
create_table "audit_events", force: :cascade do |t|
......
......@@ -48,7 +48,8 @@ Example response:
"koding_url": null,
"plantuml_enabled": false,
"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
| `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. |
| `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
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:
"koding_url": null,
"plantuml_enabled": false,
"plantuml_url": null,
"terminal_max_session_time": 0
"terminal_max_session_time": 0,
"polling_interval_multiplier": 1.0
}
```
......@@ -352,7 +352,7 @@ Example values:
export CI_JOB_ID="50"
export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a"
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_JOB_NAME="spec:other"
export CI_JOB_STAGE="test"
......
......@@ -66,14 +66,13 @@ Below is the table of events users can be notified of:
In all of the below cases, the notification will be sent to:
- Participants:
- the author and assignee of the issue/merge request
- the author of the pipeline
- authors of comments on the issue/merge request
- anyone mentioned by `@username` in the issue/merge request title or description
- anyone mentioned by `@username` in any of the comments on the issue/merge request
...with notification level "Participating" or higher
- Watchers: users with notification level "Watch" (however successful pipeline would be off for watchers)
- Watchers: users with notification level "Watch"
- Subscribers: anyone who manually subscribed to the issue/merge request
- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below
......@@ -89,8 +88,8 @@ In all of the below cases, the notification will be sent to:
| Reopen merge request | |
| Merge merge request | |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
| Failed pipeline | The above, plus the author of the pipeline |
| Successful pipeline | The above, plus the author of the pipeline |
| Failed pipeline | The author of the pipeline |
| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
In addition, if the title or description of an Issue or Merge Request is
......
......@@ -3,6 +3,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
include SharedPaths
include SharedProject
include SharedUser
include WaitForAjax
step '"John Doe" is a developer of project "Shop"' do
project.team << [john_doe, :developer]
......@@ -138,6 +139,8 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
step 'I should be directed to the corresponding page' do
page.should have_css('.identifier', text: 'Merge Request !1')
# Merge request page loads and issues a number of Ajax requests
wait_for_ajax
end
def should_see_todo(position, title, body, state: :pending)
......
......@@ -23,13 +23,13 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
end
step 'I submit new hook' do
@url = FFaker::Internet.uri("http")
@url = 'http://example.org/1'
fill_in "hook_url", with: @url
expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1)
end
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
check "hook_enable_ssl_verification"
expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1)
......
......@@ -24,8 +24,5 @@ Capybara.ignore_hidden_elements = false
Capybara::Screenshot.prune_strategy = :keep_last_run
Spinach.hooks.before_run do
require 'spinach/capybara'
require 'capybara/rails'
TestEnv.eager_load_driver_server
end
......@@ -5,10 +5,6 @@ ENV['RAILS_ENV'] = 'test'
require './config/environment'
require 'rspec/expectations'
require_relative 'capybara'
require_relative 'db_cleaner'
require_relative 'rerun'
if ENV['CI']
require 'knapsack'
Knapsack::Adapters::SpinachAdapter.bind
......
......@@ -121,7 +121,7 @@ module API
end
def oauth2_bearer_token_error_handler
Proc.new do |e|
proc do |e|
response =
case e
when MissingTokenError
......
......@@ -204,7 +204,7 @@ module API
expose :id, :name, :type, :path
expose :mode do |obj, options|
filemode = obj.mode.to_s(8)
filemode = obj.mode
filemode = "0" + filemode if filemode.length < 6
filemode
end
......@@ -581,6 +581,7 @@ module API
expose :plantuml_enabled
expose :plantuml_url
expose :terminal_max_session_time
expose :polling_interval_multiplier
end
class Release < Grape::Entity
......
......@@ -50,10 +50,14 @@ module API
forbidden!('Job has been erased!') if job.erased?
end
def authenticate_job!(job)
def authenticate_job!
job = Ci::Build.find_by_id(params[:id])
validate_job!(job) do
forbidden! unless job_token_valid?(job)
end
job
end
def job_token_valid?(job)
......
......@@ -113,8 +113,7 @@ module API
optional :state, type: String, desc: %q(Job's status: success, failed)
end
put '/:id' do
job = Ci::Build.find_by_id(params[:id])
authenticate_job!(job)
job = authenticate_job!
job.update_attributes(trace: params[:trace]) if params[:trace]
......@@ -140,8 +139,7 @@ module API
optional :token, type: String, desc: %q(Job's authentication token)
end
patch '/:id/trace' do
job = Ci::Build.find_by_id(params[:id])
authenticate_job!(job)
job = authenticate_job!
error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
content_range = request.headers['Content-Range']
......@@ -175,8 +173,7 @@ module API
require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers)
job = Ci::Build.find_by_id(params[:id])
authenticate_job!(job)
job = authenticate_job!
forbidden!('Job is not running') unless job.running?
if params[:filesize]
......@@ -212,8 +209,7 @@ module API
not_allowed! unless Gitlab.config.artifacts.enabled
require_gitlab_workhorse!
job = Ci::Build.find_by_id(params[:id])
authenticate_job!(job)
job = authenticate_job!
forbidden!('Job is not running!') unless job.running?
artifacts_upload_path = ArtifactUploader.artifacts_upload_path
......@@ -245,8 +241,7 @@ module API
optional :token, type: String, desc: %q(Job's authentication token)
end
get '/:id/artifacts' do
job = Ci::Build.find_by_id(params[:id])
authenticate_job!(job)
job = authenticate_job!
artifacts_file = job.artifacts_file
unless artifacts_file.file_storage?
......
......@@ -110,6 +110,7 @@ module API
requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run."
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 :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,
:default_group_visibility, :restricted_visibility_levels, :import_sources,
:enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
......@@ -125,7 +126,7 @@ module API
:akismet_enabled, :admin_notification_email, :sentry_enabled,
:repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_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
put "application/settings" do
attrs = declared_params(include_missing: false)
......
......@@ -293,7 +293,7 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user
::Users::DestroyService.new(current_user).execute(user)
DeleteUserWorker.perform_async(current_user.id, user.id)
end
desc 'Block a user. Available only for admins.'
......
......@@ -86,8 +86,7 @@ module Ci
# Example Request:
# PATCH /builds/:id/trace.txt
patch ":id/trace.txt" do
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
build = authenticate_build!
error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
content_range = request.headers['Content-Range']
......@@ -117,8 +116,7 @@ module Ci
require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers)
not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
build = authenticate_build!
forbidden!('build is not running') unless build.running?
if params[:filesize]
......@@ -154,8 +152,7 @@ module Ci
post ":id/artifacts" do
require_gitlab_workhorse!
not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
build = authenticate_build!
forbidden!('Build is not running!') unless build.running?
artifacts_upload_path = ArtifactUploader.artifacts_upload_path
......@@ -189,8 +186,7 @@ module Ci
# Example Request:
# GET /builds/:id/artifacts
get ":id/artifacts" do
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
build = authenticate_build!
artifacts_file = build.artifacts_file
unless artifacts_file.file_storage?
......@@ -214,8 +210,7 @@ module Ci
# Example Request:
# DELETE /builds/:id/artifacts
delete ":id/artifacts" do
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
build = authenticate_build!
status(200)
build.erase_artifacts!
......
......@@ -13,10 +13,14 @@ module Ci
forbidden! unless current_runner
end
def authenticate_build!(build)
def authenticate_build!
build = Ci::Build.find_by_id(params[:id])
validate_build!(build) do
forbidden! unless build_token_valid?(build)
end
build
end
def validate_build!(build)
......
......@@ -18,8 +18,7 @@ module Gitlab
if_none_match = env['HTTP_IF_NONE_MATCH']
if if_none_match == etag
Gitlab::Metrics.add_event(:etag_caching_cache_hit)
[304, { 'ETag' => etag }, ['']]
handle_cache_hit(etag)
else
track_cache_miss(if_none_match, cached_value_present)
......@@ -52,6 +51,14 @@ module Gitlab
%Q{W/"#{value}"}
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)
if if_none_match.blank?
Gitlab::Metrics.add_event(:etag_caching_header_missing)
......
......@@ -33,7 +33,7 @@ module Gitlab
root_id: root_tree.oid,
name: entry[:name],
type: entry[:type],
mode: entry[:filemode],
mode: entry[:filemode].to_s(8),
path: path ? File.join(path, entry[:name]) : entry[:name],
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