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
This commit is contained in:
commit
44321b1a3d
|
@ -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.
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
/* 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/* 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
/* 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));
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
/* 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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
/* 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));
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
/* 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));
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/* 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));
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/* 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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/* 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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/* 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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/* 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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/* 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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
/* 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,19 +278,26 @@
|
|||
let search;
|
||||
const locationSearch = window.location.search;
|
||||
|
||||
if (locationSearch.length === 0) {
|
||||
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;
|
||||
}, {});
|
||||
|
||||
parameters[param] = value;
|
||||
|
||||
const toString = Object.keys(parameters)
|
||||
.map(val => `${val}=${encodeURIComponent(parameters[val])}`)
|
||||
.join('&');
|
||||
|
||||
search = `?${toString}`;
|
||||
} else {
|
||||
search = `?${param}=${value}`;
|
||||
}
|
||||
|
||||
if (locationSearch.indexOf(param) !== -1) {
|
||||
const regex = new RegExp(param + '=\\d');
|
||||
search = locationSearch.replace(regex, `${param}=${value}`);
|
||||
}
|
||||
|
||||
if (locationSearch.length && locationSearch.indexOf(param) === -1) {
|
||||
search = `${locationSearch}&${param}=${value}`;
|
||||
}
|
||||
|
||||
return search;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* 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 {
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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)
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
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'
|
||||
|
|
|
@ -1,29 +1,23 @@
|
|||
- 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]
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
.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")
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
class RepositoryImportWorker
|
||||
include Sidekiq::Worker
|
||||
include Gitlab::ShellAdapter
|
||||
include DedicatedSidekiqQueue
|
||||
|
||||
attr_accessor :project, :current_user
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix symlink icon in project tree
|
||||
merge_request: 9780
|
||||
author: mhasbini
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Remove no-new annotation from file_template_mediator.js.
|
||||
merge_request: !9782
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix GitHub Importer for PRs of deleted forked repositories
|
||||
merge_request: 9992
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix redirection after login when the referer have params
|
||||
merge_request:
|
||||
author: mhasbini
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fixes method not replacing URL parameters correctly and breaking pipelines
|
||||
pagination
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Backport API changes needed to fix sticking in EE
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Drop support for correctly processing legacy pipelines
|
||||
merge_request: 10266
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Improve performance of GitHub importer for large repositories.
|
||||
merge_request: 10273
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Introduce "polling_interval_multiplier" as application setting
|
||||
merge_request: 10280
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix project creation failure due to race condition in namespace directory creation
|
||||
merge_request: 10268
|
||||
author: Robin Bobbitt
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Log errors during generating of Gitlab Pages to debug log
|
||||
merge_request: 10335
|
||||
author: Danilo Bargen
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Only email pipeline creators; only email for successful pipelines with custom
|
||||
settings
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix race condition where a namespace would be deleted before a project was
|
||||
deleted
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Relax constraint on Wiki IDs, since subdirectories can contain spaces
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Enable Style/Proc cop for rubocop
|
||||
merge_request:
|
||||
author: mhasbini
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
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
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
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
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
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
|
||||
|
|
|
@ -1,5 +1,27 @@
|
|||
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
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# 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,
|
||||
)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue