Commit 7f577bda authored by Mike Greiling's avatar Mike Greiling

Merge branch 'master' into 91-milestone-burndown-charts

* master: (95 commits)
  Port 'Refactor test_utils bundle' to EE
  Port of 28732-expandable-folders to EE
  Allow to edit build minutes per user ee
  Resolve merge conflicts
  Backport differences in global search from EE to CE
  Fix elasticsearch global code search following https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9655
  Fix sticking of the database load balancer
  Remove useless queries with false conditions (e.g 1=0)
  Don't autofill kubernetes namespace
  Split status and confidentiality action
  Update patch_versions.md, add `yarn:install` in `Clean up assets and cache` command.
  Fix conflcit in repository spec
  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 outside the search field
  Remove index for users.current sign in at
  Port of fix-github-importer-slowness to EE
  Enable the `bullet_logger` setting; enable `raise` in test environment
  Fix Rubocop offenses
  Set the right timeout for Gitlab::Shell#fetch_remote
  ...
parents 38a666e6 2019c376
...@@ -535,6 +535,10 @@ Style/WhileUntilModifier: ...@@ -535,6 +535,10 @@ Style/WhileUntilModifier:
Style/WordArray: Style/WordArray:
Enabled: true Enabled: true
# Use `proc` instead of `Proc.new`.
Style/Proc:
Enabled: true
# Metrics ##################################################################### # Metrics #####################################################################
# A calculated magnitude based on number of assignments, # A calculated magnitude based on number of assignments,
......
...@@ -226,12 +226,8 @@ Style/PredicateName: ...@@ -226,12 +226,8 @@ Style/PredicateName:
Style/PreferredHashMethods: Style/PreferredHashMethods:
Enabled: false Enabled: false
# Offense count: 9
# Cop supports --auto-correct.
Style/Proc:
Enabled: false
# Offense count: 64 # Offense count: 62
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, exploded # SupportedStyles: compact, exploded
......
...@@ -271,7 +271,6 @@ group :development do ...@@ -271,7 +271,6 @@ group :development do
gem 'brakeman', '~> 3.6.0', require: false gem 'brakeman', '~> 3.6.0', require: false
gem 'letter_opener_web', '~> 1.3.0' gem 'letter_opener_web', '~> 1.3.0'
gem 'bullet', '~> 5.5.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
# Better errors handler # Better errors handler
...@@ -283,6 +282,7 @@ group :development do ...@@ -283,6 +282,7 @@ group :development do
end end
group :development, :test do group :development, :test do
gem 'bullet', '~> 5.5.0', require: !!ENV['ENABLE_BULLET']
gem 'pry-byebug', '~> 3.4.1', platform: :mri gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'pry-rails', '~> 0.3.4' gem 'pry-rails', '~> 0.3.4'
...@@ -363,4 +363,4 @@ gem 'vmstat', '~> 2.3.0' ...@@ -363,4 +363,4 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.3.0' gem 'gitaly', '~> 0.5.0'
...@@ -277,7 +277,7 @@ GEM ...@@ -277,7 +277,7 @@ GEM
json json
get_process_mem (0.2.0) get_process_mem (0.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.3.0) gitaly (0.5.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -940,7 +940,7 @@ DEPENDENCIES ...@@ -940,7 +940,7 @@ DEPENDENCIES
fuubar (~> 2.0.0) fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0) gemojione (~> 3.0)
gitaly (~> 0.3.0) gitaly (~> 0.5.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-elasticsearch-git (= 1.1.1) gitlab-elasticsearch-git (= 1.1.1)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
......
/* eslint-disable class-methods-use-this */
/* global Flash */
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector';
import LicenseSelector from './template_selectors/license_selector';
export default class FileTemplateMediator {
constructor({ editor, currentAction }) {
this.editor = editor;
this.currentAction = currentAction;
this.initTemplateSelectors();
this.initTemplateTypeSelector();
this.initDomElements();
this.initDropdowns();
this.initPageEvents();
}
initTemplateSelectors() {
// Order dictates template type dropdown item order
this.templateSelectors = [
GitignoreSelector,
BlobCiYamlSelector,
DockerfileSelector,
LicenseSelector,
].map(TemplateSelectorClass => new TemplateSelectorClass({ mediator: this }));
}
initTemplateTypeSelector() {
this.typeSelector = new FileTemplateTypeSelector({
mediator: this,
dropdownData: this.templateSelectors
.map((templateSelector) => {
const cfg = templateSelector.config;
return {
name: cfg.name,
key: cfg.key,
};
}),
});
}
initDomElements() {
const $templatesMenu = $('.template-selectors-menu');
const $undoMenu = $templatesMenu.find('.template-selectors-undo-menu');
const $fileEditor = $('.file-editor');
this.$templatesMenu = $templatesMenu;
this.$undoMenu = $undoMenu;
this.$undoBtn = $undoMenu.find('button');
this.$templateSelectors = $templatesMenu.find('.template-selector-dropdowns-wrap');
this.$filenameInput = $fileEditor.find('.js-file-path-name-input');
this.$fileContent = $fileEditor.find('#file-content');
this.$commitForm = $fileEditor.find('form');
this.$navLinks = $fileEditor.find('.nav-links');
}
initDropdowns() {
if (this.currentAction === 'create') {
this.typeSelector.show();
} else {
this.hideTemplateSelectorMenu();
}
this.displayMatchedTemplateSelector();
}
initPageEvents() {
this.listenForFilenameInput();
this.prepFileContentForSubmit();
this.listenForPreviewMode();
}
listenForFilenameInput() {
this.$filenameInput.on('keyup blur', () => {
this.displayMatchedTemplateSelector();
});
}
prepFileContentForSubmit() {
this.$commitForm.submit(() => {
this.$fileContent.val(this.editor.getValue());
});
}
listenForPreviewMode() {
this.$navLinks.on('click', 'a', (e) => {
const urlPieces = e.target.href.split('#');
const hash = urlPieces[1];
if (hash === 'preview') {
this.hideTemplateSelectorMenu();
} else if (hash === 'editor') {
this.showTemplateSelectorMenu();
}
});
}
selectTemplateType(item, el, e) {
if (e) {
e.preventDefault();
}
this.templateSelectors.forEach((selector) => {
if (selector.config.key === item.key) {
selector.show();
} else {
selector.hide();
}
});
this.typeSelector.setToggleText(item.name);
this.cacheToggleText();
}
selectTemplateFile(selector, query, data) {
selector.renderLoading();
// in case undo menu is already already there
this.destroyUndoMenu();
this.fetchFileTemplate(selector.config.endpoint, query, data)
.then((file) => {
this.showUndoMenu();
this.setEditorContent(file);
this.setFilename(selector.config.name);
selector.renderLoaded();
})
.catch(err => new Flash(`An error occurred while fetching the template: ${err}`));
}
displayMatchedTemplateSelector() {
const currentInput = this.getFilename();
this.templateSelectors.forEach((selector) => {
const match = selector.config.pattern.test(currentInput);
if (match) {
this.typeSelector.show();
this.selectTemplateType(selector.config);
this.showTemplateSelectorMenu();
}
});
}
fetchFileTemplate(apiCall, query, data) {
return new Promise((resolve) => {
const resolveFile = file => resolve(file);
if (!data) {
apiCall(query, resolveFile);
} else {
apiCall(query, data, resolveFile);
}
});
}
setEditorContent(file) {
if (!file && file !== '') return;
const newValue = file.content || file;
this.editor.setValue(newValue, 1);
this.editor.focus();
this.editor.navigateFileStart();
}
findTemplateSelectorByKey(key) {
return this.templateSelectors.find(selector => selector.config.key === key);
}
showUndoMenu() {
this.$undoMenu.removeClass('hidden');
this.$undoBtn.on('click', () => {
this.restoreFromCache();
this.destroyUndoMenu();
});
}
destroyUndoMenu() {
this.cacheFileContents();
this.cacheToggleText();
this.$undoMenu.addClass('hidden');
this.$undoBtn.off('click');
}
hideTemplateSelectorMenu() {
this.$templatesMenu.hide();
}
showTemplateSelectorMenu() {
this.$templatesMenu.show();
}
cacheToggleText() {
this.cachedToggleText = this.getTemplateSelectorToggleText();
}
cacheFileContents() {
this.cachedContent = this.editor.getValue();
this.cachedFilename = this.getFilename();
}
restoreFromCache() {
this.setEditorContent(this.cachedContent);
this.setFilename(this.cachedFilename);
this.setTemplateSelectorToggleText();
}
getTemplateSelectorToggleText() {
return this.$templateSelectors
.find('.js-template-selector-wrap:visible .dropdown-toggle-text')
.text();
}
setTemplateSelectorToggleText() {
return this.$templateSelectors
.find('.js-template-selector-wrap:visible .dropdown-toggle-text')
.text(this.cachedToggleText);
}
getTypeSelectorToggleText() {
return this.typeSelector.getToggleText();
}
getFilename() {
return this.$filenameInput.val();
}
setFilename(name) {
this.$filenameInput.val(name);
}
getSelected() {
return this.templateSelectors.find(selector => selector.selected);
}
}
/* global Api */
export default class FileTemplateSelector {
constructor(mediator) {
this.mediator = mediator;
this.$dropdown = null;
this.$wrapper = null;
}
init() {
const cfg = this.config;
this.$dropdown = $(cfg.dropdown);
this.$wrapper = $(cfg.wrapper);
this.$loadingIcon = this.$wrapper.find('.fa-chevron-down');
this.$dropdownToggleText = this.$wrapper.find('.dropdown-toggle-text');
this.initDropdown();
}
show() {
if (this.$dropdown === null) {
this.init();
}
this.$wrapper.removeClass('hidden');
}
hide() {
if (this.$dropdown !== null) {
this.$wrapper.addClass('hidden');
}
}
getToggleText() {
return this.$dropdownToggleText.text();
}
setToggleText(text) {
this.$dropdownToggleText.text(text);
}
renderLoading() {
this.$loadingIcon
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
}
renderLoaded() {
this.$loadingIcon
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
}
reportSelection(query, el, e, data) {
e.preventDefault();
return this.mediator.selectTemplateFile(this, query, data);
}
}
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobCiYamlSelector extends TemplateSelector {
requestFile(query) {
return Api.gitlabCiYml(query.name, (file, config) => this.setEditorContent(file, config));
}
}
/* global Api */
import BlobCiYamlSelector from './blob_ci_yaml_selector';
export default class BlobCiYamlSelectors {
constructor({ editor, $dropdowns }) {
this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
this.initSelectors(editor);
}
initSelectors(editor) {
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobCiYamlSelector({
editor,
pattern: /(.gitlab-ci.yml)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobDockerfileSelector extends TemplateSelector {
requestFile(query) {
return Api.dockerfileYml(query.name, (file, config) => this.setEditorContent(file, config));
}
}
import BlobDockerfileSelector from './blob_dockerfile_selector';
export default class BlobDockerfileSelectors {
constructor({ editor, $dropdowns }) {
this.editor = editor;
this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
this.initSelectors();
}
initSelectors() {
const editor = this.editor;
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobDockerfileSelector({
editor,
pattern: /(Dockerfile)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobGitignoreSelector extends TemplateSelector {
requestFile(query) {
return Api.gitignoreText(query.name, (file, config) => this.setEditorContent(file, config));
}
}
import BlobGitignoreSelector from './blob_gitignore_selector';
export default class BlobGitignoreSelectors {
constructor({ editor, $dropdowns }) {
this.$dropdowns = $dropdowns || $('.js-gitignore-selector');
this.editor = editor;
this.initSelectors();
}
initSelectors() {
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobGitignoreSelector({
pattern: /(.gitignore)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
dropdown: $dropdown,
editor: this.editor,
});
});
}
}
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobLicenseSelector extends TemplateSelector {
requestFile(query) {
const data = {
project: this.dropdown.data('project'),
fullname: this.dropdown.data('fullname'),
};
return Api.licenseText(query.id, data, (file, config) => this.setEditorContent(file, config));
}
}
/* eslint-disable no-unused-vars, no-param-reassign */
import BlobLicenseSelector from './blob_license_selector';
export default class BlobLicenseSelectors {
constructor({ $dropdowns, editor }) {
this.$dropdowns = $dropdowns || $('.js-license-selector');
this.initSelectors(editor);
}
initSelectors(editor) {
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobLicenseSelector({
editor,
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class BlobCiYamlSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'gitlab-ci-yaml',
name: '.gitlab-ci.yml',
pattern: /(.gitlab-ci.yml)/,
endpoint: Api.gitlabCiYml,
dropdown: '.js-gitlab-ci-yml-selector',
wrapper: '.js-gitlab-ci-yml-selector-wrap',
};
}
initDropdown() {
// maybe move to super class as well
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
text: item => item.name,
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class DockerfileSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'dockerfile',
name: 'Dockerfile',
pattern: /(Dockerfile)/,
endpoint: Api.dockerfileYml,
dropdown: '.js-dockerfile-selector',
wrapper: '.js-dockerfile-selector-wrap',
};
}
initDropdown() {
// maybe move to super class as well
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
text: item => item.name,
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class BlobGitignoreSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'gitignore',
name: '.gitignore',
pattern: /(.gitignore)/,
endpoint: Api.gitignoreText,
dropdown: '.js-gitignore-selector',
wrapper: '.js-gitignore-selector-wrap',
};
}
initDropdown() {
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
text: item => item.name,
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class BlobLicenseSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'license',
name: 'LICENSE',
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
endpoint: Api.licenseText,
dropdown: '.js-license-selector',
wrapper: '.js-license-selector-wrap',
};
}
initDropdown() {
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => {
const data = {
project: this.$dropdown.data('project'),
fullname: this.$dropdown.data('fullname'),
};
this.reportSelection(query.id, el, e, data);
},
text: item => item.name,
});
}
}
import FileTemplateSelector from '../file_template_selector';
export default class FileTemplateTypeSelector extends FileTemplateSelector {
constructor({ mediator, dropdownData }) {
super(mediator);
this.mediator = mediator;
this.config = {
dropdown: '.js-template-type-selector',
wrapper: '.js-template-type-selector-wrap',
dropdownData,
};
}
initDropdown() {
this.$dropdown.glDropdown({
data: this.config.dropdownData,
filterable: false,
selectable: true,
toggleLabel: item => item.name,
clicked: (item, el, e) => this.mediator.selectTemplateType(item, el, e),
text: item => item.name,
});
}
}
...@@ -13,8 +13,9 @@ $(() => { ...@@ -13,8 +13,9 @@ $(() => {
const urlRoot = editBlobForm.data('relative-url-root'); const urlRoot = editBlobForm.data('relative-url-root');
const assetsPath = editBlobForm.data('assets-prefix'); const assetsPath = editBlobForm.data('assets-prefix');
const blobLanguage = editBlobForm.data('blob-language'); const blobLanguage = editBlobForm.data('blob-language');
const currentAction = $('.js-file-title').data('current-action');
new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage); new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction);
new NewCommitForm(editBlobForm); new NewCommitForm(editBlobForm);
} }
......
/* global ace */ /* global ace */
import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors'; import TemplateSelectorMediator from '../blob/file_template_mediator';
import BlobGitignoreSelectors from '../blob/template_selectors/blob_gitignore_selectors';
import BlobCiYamlSelectors from '../blob/template_selectors/blob_ci_yaml_selectors';
import BlobDockerfileSelectors from '../blob/template_selectors/blob_dockerfile_selectors';
export default class EditBlob { export default class EditBlob {
constructor(assetsPath, aceMode) { constructor(assetsPath, aceMode, currentAction) {
this.configureAceEditor(aceMode, assetsPath); this.configureAceEditor(aceMode, assetsPath);
this.prepFileContentForSubmit();
this.initModePanesAndLinks(); this.initModePanesAndLinks();
this.initSoftWrap(); this.initSoftWrap();
this.initFileSelectors(); this.initFileSelectors(currentAction);
} }
configureAceEditor(aceMode, assetsPath) { configureAceEditor(aceMode, assetsPath) {
...@@ -19,6 +15,10 @@ export default class EditBlob { ...@@ -19,6 +15,10 @@ export default class EditBlob {
ace.config.loadModule('ace/ext/searchbox'); ace.config.loadModule('ace/ext/searchbox');
this.editor = ace.edit('editor'); this.editor = ace.edit('editor');
// This prevents warnings re: automatic scrolling being logged
this.editor.$blockScrolling = Infinity;
this.editor.focus(); this.editor.focus();
if (aceMode) { if (aceMode) {
...@@ -26,29 +26,13 @@ export default class EditBlob { ...@@ -26,29 +26,13 @@ export default class EditBlob {
} }
} }
prepFileContentForSubmit() { initFileSelectors(currentAction) {
$('form').submit(() => { this.fileTemplateMediator = new TemplateSelectorMediator({
$('#file-content').val(this.editor.getValue()); currentAction,
editor: this.editor,
}); });
} }
initFileSelectors() {
this.blobTemplateSelectors = [
new BlobLicenseSelectors({
editor: this.editor,
}),
new BlobGitignoreSelectors({
editor: this.editor,
}),
new BlobCiYamlSelectors({
editor: this.editor,
}),
new BlobDockerfileSelectors({
editor: this.editor,
}),
];
}
initModePanesAndLinks() { initModePanesAndLinks() {
this.$editModePanes = $('.js-edit-mode-pane'); this.$editModePanes = $('.js-edit-mode-pane');
this.$editModeLinks = $('.js-edit-mode a'); this.$editModeLinks = $('.js-edit-mode a');
......
...@@ -335,6 +335,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -335,6 +335,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:repository:show': case 'projects:repository:show':
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
new UsersSelect();
break; break;
case 'projects:ci_cd:show': case 'projects:ci_cd:show':
new gl.ProjectVariables(); new gl.ProjectVariables();
......
...@@ -25,6 +25,7 @@ export default Vue.component('environment-component', { ...@@ -25,6 +25,7 @@ export default Vue.component('environment-component', {
state: store.state, state: store.state,
visibility: 'available', visibility: 'available',
isLoading: false, isLoading: false,
isLoadingFolderContent: false,
cssContainerClass: environmentsData.cssClass, cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint, endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment, canCreateDeployment: environmentsData.canCreateDeployment,
...@@ -79,10 +80,12 @@ export default Vue.component('environment-component', { ...@@ -79,10 +80,12 @@ export default Vue.component('environment-component', {
this.fetchEnvironments(); this.fetchEnvironments();
eventHub.$on('refreshEnvironments', this.fetchEnvironments); eventHub.$on('refreshEnvironments', this.fetchEnvironments);
eventHub.$on('toggleFolder', this.toggleFolder);
}, },
beforeDestroyed() { beforeDestroyed() {
eventHub.$off('refreshEnvironments'); eventHub.$off('refreshEnvironments');
eventHub.$off('toggleFolder');
}, },
methods: { methods: {
...@@ -97,6 +100,14 @@ export default Vue.component('environment-component', { ...@@ -97,6 +100,14 @@ export default Vue.component('environment-component', {
return this.store.toggleDeployBoard(model.id); return this.store.toggleDeployBoard(model.id);
}, },
toggleFolder(folder, folderUrl) {
this.store.toggleFolder(folder);
if (!folder.isOpen) {
this.fetchChildEnvironments(folder, folderUrl);
}
},
/** /**
* Will change the page number and update the URL. * Will change the page number and update the URL.
* *
...@@ -135,6 +146,21 @@ export default Vue.component('environment-component', { ...@@ -135,6 +146,21 @@ export default Vue.component('environment-component', {
new Flash('An error occurred while fetching the environments.'); new Flash('An error occurred while fetching the environments.');
}); });
}, },
fetchChildEnvironments(folder, folderUrl) {
this.isLoadingFolderContent = true;
this.service.getFolderContent(folderUrl)
.then(resp => resp.json())
.then((response) => {
this.store.setfolderContent(folder, response.environments);
this.isLoadingFolderContent = false;
})
.catch(() => {
this.isLoadingFolderContent = false;
new Flash('An error occurred while fetching the environments.');
});
},
}, },
template: ` template: `
...@@ -199,7 +225,8 @@ export default Vue.component('environment-component', { ...@@ -199,7 +225,8 @@ export default Vue.component('environment-component', {
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:toggleDeployBoard="toggleDeployBoard" :toggleDeployBoard="toggleDeployBoard"
:store="store" :store="store"
:service="service"/> :service="service"
:is-loading-folder-content="isLoadingFolderContent" />
</div> </div>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
......
...@@ -13,6 +13,7 @@ import RollbackComponent from './environment_rollback'; ...@@ -13,6 +13,7 @@ import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button'; import TerminalButtonComponent from './environment_terminal_button';
import MonitoringButtonComponent from './environment_monitoring'; import MonitoringButtonComponent from './environment_monitoring';
import CommitComponent from '../../vue_shared/components/commit'; import CommitComponent from '../../vue_shared/components/commit';
import eventHub from '../event_hub';
const timeagoInstance = new Timeago(); const timeagoInstance = new Timeago();
...@@ -434,8 +435,14 @@ export default { ...@@ -434,8 +435,14 @@ export default {
return true; return true;
}, },
methods: {
onClickFolder() {
eventHub.$emit('toggleFolder', this.model, this.folderUrl);
},
},
template: ` template: `
<tr> <tr :class="{ 'js-child-row': model.isChildren }">
<td> <td>
<span class="deploy-board-icon" <span class="deploy-board-icon"
v-if="model.hasDeployBoard" v-if="model.hasDeployBoard"
...@@ -443,22 +450,38 @@ export default { ...@@ -443,22 +450,38 @@ export default {
<i v-show="!model.isDeployBoardVisible" <i v-show="!model.isDeployBoardVisible"
class="fa fa-caret-right" class="fa fa-caret-right"
aria-hidden="true"> aria-hidden="true" />
</i>
<i v-show="model.isDeployBoardVisible" <i v-show="model.isDeployBoardVisible"
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true"> aria-hidden="true" />
</i>
</span> </span>
<a v-if="!model.isFolder" <a v-if="!model.isFolder"
class="environment-name" class="environment-name"
:class="{ 'prepend-left-default': model.isChildren }"
:href="environmentPath"> :href="environmentPath">
{{model.name}} {{model.name}}
</a> </a>
<a v-else class="folder-name" :href="folderUrl"> <span v-if="model.isFolder"
class="folder-name"
@click="onClickFolder"
role="button">
<span class="folder-icon">
<i
v-show="model.isOpen"
class="fa fa-caret-down"
aria-hidden="true" />
<i
v-show="!model.isOpen"
class="fa fa-caret-right"
aria-hidden="true"/>
</span>
<span class="folder-icon"> <span class="folder-icon">
<i class="fa fa-folder" aria-hidden="true"></i> <i class="fa fa-folder" aria-hidden="true"></i>
</span> </span>
...@@ -470,7 +493,7 @@ export default { ...@@ -470,7 +493,7 @@ export default {
<span class="badge"> <span class="badge">
{{model.size}} {{model.size}}
</span> </span>
</a> </span>
</td> </td>
<td class="deployment-column"> <td class="deployment-column">
......
...@@ -49,6 +49,18 @@ export default { ...@@ -49,6 +49,18 @@ export default {
required: true, required: true,
default: () => ({}), default: () => ({}),
}, },
isLoadingFolderContent: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
folderUrl(model) {
return `${window.location.pathname}/folders/${model.folderName}`;
},
}, },
template: ` template: `
...@@ -85,6 +97,31 @@ export default { ...@@ -85,6 +97,31 @@ export default {
</deploy-board> </deploy-board>
</td> </td>
</tr> </tr>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<tr v-if="isLoadingFolderContent">
<td colspan="6" class="text-center">
<i class="fa fa-spin fa-spinner fa-2x" aria-hidden="true"/>
</td>
</tr>
<template v-else>
<tr is="environment-item"
v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service"></tr>
<tr>
<td colspan="6" class="text-center">
<a :href="folderUrl(model)" class="btn btn-default">
Show all
</a>
</td>
</tr>
</template>
</template>
</template> </template>
</tbody> </tbody>
</table> </table>
......
...@@ -7,6 +7,7 @@ Vue.use(VueResource); ...@@ -7,6 +7,7 @@ Vue.use(VueResource);
export default class EnvironmentsService { export default class EnvironmentsService {
constructor(endpoint) { constructor(endpoint) {
this.environments = Vue.resource(endpoint); this.environments = Vue.resource(endpoint);
this.folderResults = 3;
} }
get(scope, page) { get(scope, page) {
...@@ -20,4 +21,8 @@ export default class EnvironmentsService { ...@@ -20,4 +21,8 @@ export default class EnvironmentsService {
postAction(endpoint) { postAction(endpoint) {
return Vue.http.post(endpoint, {}, { emulateJSON: true }); return Vue.http.post(endpoint, {}, { emulateJSON: true });
} }
getFolderContent(folderUrl) {
return Vue.http.get(`${folderUrl}.json?per_page=${this.folderResults}`);
}
} }
...@@ -45,23 +45,29 @@ export default class EnvironmentsStore { ...@@ -45,23 +45,29 @@ export default class EnvironmentsStore {
const filteredEnvironments = environments.map((env) => { const filteredEnvironments = environments.map((env) => {
let filtered = {}; let filtered = {};
if (env.size > 1) {
filtered = Object.assign({}, env, {
isFolder: true,
folderName: env.name,
isOpen: false,
children: [],
});
}
if (env.latest) { if (env.latest) {
filtered = Object.assign({}, env, env.latest); filtered = Object.assign(filtered, env, env.latest);
delete filtered.latest; delete filtered.latest;
} else { } else {
filtered = Object.assign({}, env); filtered = Object.assign(filtered, env);
} }
if (filtered.size > 1) { if (filtered.size === 1 && filtered.rollout_status_path) {
filtered = Object.assign(filtered, env, { isFolder: true, folderName: env.name }); filtered = Object.assign({}, filtered, {
} else if (filtered.size === 1 && filtered.rollout_status_path) {
filtered = Object.assign({}, env, filtered, {
hasDeployBoard: true, hasDeployBoard: true,
isDeployBoardVisible: false, isDeployBoardVisible: false,
deployBoardData: {}, deployBoardData: {},
}); });
} }
return filtered; return filtered;
}); });
...@@ -155,4 +161,66 @@ export default class EnvironmentsStore { ...@@ -155,4 +161,66 @@ export default class EnvironmentsStore {
}); });
return this.state.environments; return this.state.environments;
} }
/*
* Toggles folder open property for the given folder.
*
* @param {Object} folder
* @return {Array}
*/
toggleFolder(folder) {
return this.updateFolder(folder, 'isOpen', !folder.isOpen);
}
/**
* Updates the folder with the received environments.
*
*
* @param {Object} folder Folder to update
* @param {Array} environments Received environments
* @return {Object}
*/
setfolderContent(folder, environments) {
const updatedEnvironments = environments.map((env) => {
let updated = env;
if (env.latest) {
updated = Object.assign({}, env, env.latest);
delete updated.latest;
} else {
updated = env;
}
updated.isChildren = true;
return updated;
});
return this.updateFolder(folder, 'children', updatedEnvironments);
}
/**
* Given a folder a prop and a new value updates the correct folder.
*
* @param {Object} folder
* @param {String} prop
* @param {String|Boolean|Object|Array} newValue
* @return {Array}
*/
updateFolder(folder, prop, newValue) {
const environments = this.state.environments;
const updatedEnvironments = environments.map((env) => {
const updateEnv = Object.assign({}, env);
if (env.isFolder && env.id === folder.id) {
updateEnv[prop] = newValue;
}
return updateEnv;
});
this.state.environments = updatedEnvironments;
return updatedEnvironments;
}
} }
...@@ -263,7 +263,7 @@ ...@@ -263,7 +263,7 @@
}); });
/** /**
* Updates the search parameter of a URL given the parameter and values provided. * Updates the search parameter of a URL given the parameter and value provided.
* *
* If no search params are present we'll add it. * If no search params are present we'll add it.
* If param for page is already present, we'll update it * If param for page is already present, we'll update it
...@@ -278,17 +278,24 @@ ...@@ -278,17 +278,24 @@
let search; let search;
const locationSearch = window.location.search; const locationSearch = window.location.search;
if (locationSearch.length === 0) { if (locationSearch.length) {
search = `?${param}=${value}`; const parameters = locationSearch.substring(1, locationSearch.length)
} .split('&')
.reduce((acc, element) => {
const val = element.split('=');
acc[val[0]] = decodeURIComponent(val[1]);
return acc;
}, {});
if (locationSearch.indexOf(param) !== -1) { parameters[param] = value;
const regex = new RegExp(param + '=\\d');
search = locationSearch.replace(regex, `${param}=${value}`);
}
if (locationSearch.length && locationSearch.indexOf(param) === -1) { const toString = Object.keys(parameters)
search = `${locationSearch}&${param}=${value}`; .map(val => `${val}=${encodeURIComponent(parameters[val])}`)
.join('&');
search = `?${toString}`;
} else {
search = `?${param}=${value}`;
} }
return search; return search;
......
/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */ /* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */ /* global Api */
import TemplateSelector from '../blob/template_selectors/template_selector'; import TemplateSelector from '../blob/template_selector';
((global) => { ((global) => {
class IssuableTemplateSelector extends TemplateSelector { class IssuableTemplateSelector extends TemplateSelector {
......
...@@ -21,6 +21,7 @@ export default { ...@@ -21,6 +21,7 @@ export default {
<li v-for="artifact in artifacts"> <li v-for="artifact in artifacts">
<a <a
rel="nofollow" rel="nofollow"
download
:href="artifact.path"> :href="artifact.path">
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
<span>Download {{artifact.name}} artifacts</span> <span>Download {{artifact.name}} artifacts</span>
......
...@@ -146,6 +146,10 @@ ...@@ -146,6 +146,10 @@
display: block; display: block;
} }
&.scrolling-tabs {
float: left;
}
li a { li a {
padding: 16px 15px 11px; padding: 16px 15px 11px;
} }
...@@ -480,6 +484,10 @@ ...@@ -480,6 +484,10 @@
.inner-page-scroll-tabs { .inner-page-scroll-tabs {
position: relative; position: relative;
.nav-links {
padding-bottom: 1px;
}
.fade-right { .fade-right {
@include fade(left, $white-light); @include fade(left, $white-light);
right: 0; right: 0;
......
.file-editor { .file-editor {
.nav-links {
border-top: 1px solid $border-color;
border-right: 1px solid $border-color;
border-left: 1px solid $border-color;
border-bottom: none;
border-radius: 2px;
background: $gray-normal;
}
#editor { #editor {
border: none; border: none;
border-radius: 0; border-radius: 0;
...@@ -72,11 +81,7 @@ ...@@ -72,11 +81,7 @@
} }
.encoding-selector, .encoding-selector,
.soft-wrap-toggle, .soft-wrap-toggle {
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
font-family: $regular_font; font-family: $regular_font;
...@@ -103,28 +108,9 @@ ...@@ -103,28 +108,9 @@
} }
} }
} }
.gitignore-selector,
.license-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector {
.dropdown {
line-height: 21px;
}
.dropdown-menu-toggle {
vertical-align: top;
width: 220px;
}
}
.gitlab-ci-yml-selector {
.dropdown-menu-toggle {
width: 250px;
}
}
} }
@media(max-width: $screen-xs-max){ @media(max-width: $screen-xs-max){
.file-editor { .file-editor {
.file-title { .file-title {
...@@ -149,10 +135,7 @@ ...@@ -149,10 +135,7 @@
margin: 3px 0; margin: 3px 0;
} }
.encoding-selector, .encoding-selector {
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector {
display: block; display: block;
margin: 3px 0; margin: 3px 0;
...@@ -163,3 +146,104 @@ ...@@ -163,3 +146,104 @@
} }
} }
} }
.blob-new-page-title,
.blob-edit-page-title {
margin: 19px 0 21px;
vertical-align: top;
display: inline-block;
@media(max-width: $screen-sm-max) {
display: block;
margin: 19px 0 12px;
}
}
.template-selectors-menu {
display: inline-block;
vertical-align: top;
margin: 14px 0 0 16px;
padding: 0 0 0 14px;
border-left: 1px solid $border-color;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 5px 0;
padding: 0;
border-left: none;
}
}
.templates-selectors-label {
display: inline-block;
vertical-align: top;
margin-top: 6px;
line-height: 21px;
@media(max-width: $screen-sm-max) {
display: block;
margin: 5px 0;
}
}
.template-selector-dropdowns-wrap {
display: inline-block;
margin-left: 8px;
vertical-align: top;
margin: 5px 0 0 8px;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 0 0 16px;
}
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector,
.template-type-selector {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
margin-top: -5px;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 5px 0;
}
.dropdown {
line-height: 21px;
}
.dropdown-menu-toggle {
width: 250px;
vertical-align: top;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 5px 0;
}
}
}
}
.template-selectors-undo-menu {
display: inline-block;
margin: 7px 0 0 10px;
@media(max-width: $screen-sm-max) {
display: block;
width: 100%;
margin: 20px 0;
}
button {
margin: -4px 0 0 15px;
}
}
class Admin::AbuseReportsController < Admin::ApplicationController class Admin::AbuseReportsController < Admin::ApplicationController
def index def index
@abuse_reports = AbuseReport.order(id: :desc).page(params[:page]) @abuse_reports = AbuseReport.order(id: :desc).page(params[:page])
@abuse_reports.includes(:reporter, :user)
end end
def destroy def destroy
......
...@@ -142,6 +142,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -142,6 +142,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:unique_ips_limit_enabled, :unique_ips_limit_enabled,
:version_check_enabled, :version_check_enabled,
:terminal_max_session_time, :terminal_max_session_time,
:polling_interval_multiplier,
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
import_sources: [], import_sources: [],
......
...@@ -205,7 +205,8 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -205,7 +205,8 @@ class Admin::UsersController < Admin::ApplicationController
def user_params_ee def user_params_ee
[ [
:note :note,
namespace_attributes: [:id, :shared_runners_minutes_limit]
] ]
end end
end end
...@@ -15,6 +15,9 @@ module IssuableCollections ...@@ -15,6 +15,9 @@ module IssuableCollections
# a new order into the collection. # a new order into the collection.
# We cannot use reorder to not mess up the paginated collection. # We cannot use reorder to not mess up the paginated collection.
issuable_ids = issuable_collection.map(&:id) issuable_ids = issuable_collection.map(&:id)
return {} if issuable_ids.empty?
issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type) issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
issuable_merge_requests_count = issuable_merge_requests_count =
......
...@@ -17,6 +17,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -17,6 +17,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@members = @members.search(params[:search]) if params[:search].present? @members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort) @members = @members.sort(@sort)
@members = @members.page(params[:page]).per(50) @members = @members.page(params[:page]).per(50)
@members.includes(:user)
@requesters = AccessRequestsFinder.new(@group).execute(current_user) @requesters = AccessRequestsFinder.new(@group).execute(current_user)
......
class Profiles::AccountsController < Profiles::ApplicationController class Profiles::AccountsController < Profiles::ApplicationController
include AuthHelper
def show def show
@user = current_user @user = current_user
end end
def unlink def unlink
provider = params[:provider] provider = params[:provider]
current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml' identity = current_user.identities.find_by(provider: provider)
return render_404 unless identity
if unlink_allowed?(provider)
identity.destroy
else
flash[:alert] = "You are not allowed to unlink your primary login account"
end
redirect_to profile_account_path redirect_to profile_account_path
end end
end end
...@@ -59,7 +59,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -59,7 +59,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
def render_ok def render_ok
set_workhorse_internal_api_content_type set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.git_http_ok(repository, user) render json: Gitlab::Workhorse.git_http_ok(repository, user, action_name)
end end
def render_http_not_allowed def render_http_not_allowed
......
...@@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@collection_type = "MergeRequest" @collection_type = "MergeRequest"
@merge_requests = merge_requests_collection @merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.includes(merge_request_diff: :merge_request)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
......
...@@ -21,9 +21,9 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -21,9 +21,9 @@ class Projects::MilestonesController < Projects::ApplicationController
@sort = params[:sort] || 'due_date_asc' @sort = params[:sort] || 'due_date_asc'
@milestones = @milestones.sort(@sort) @milestones = @milestones.sort(@sort)
@milestones = @milestones.includes(:project)
respond_to do |format| respond_to do |format|
format.html do format.html do
@milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page]) @milestones = @milestones.page(params[:page])
end end
format.json do format.json do
......
...@@ -25,12 +25,12 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -25,12 +25,12 @@ class RegistrationsController < Devise::RegistrationsController
end end
def destroy def destroy
Users::DestroyService.new(current_user).execute(current_user) DeleteUserWorker.perform_async(current_user.id, current_user.id)
respond_to do |format| respond_to do |format|
format.html do format.html do
session.try(:destroy) session.try(:destroy)
redirect_to new_user_session_path, notice: "Account successfully removed." redirect_to new_user_session_path, notice: "Account scheduled for removal."
end end
end end
end end
......
...@@ -6,46 +6,19 @@ class SearchController < ApplicationController ...@@ -6,46 +6,19 @@ class SearchController < ApplicationController
layout 'search' layout 'search'
def show def show
if params[:project_id].present? search_service = SearchService.new(current_user, params)
@project = Project.find_by(id: params[:project_id])
@project = nil unless can?(current_user, :download_code, @project)
end
if params[:group_id].present? @project = search_service.project
@group = Group.find_by(id: params[:group_id]) @group = search_service.group
@group = nil unless can?(current_user, :read_group, @group)
end
return if params[:search].blank? return if params[:search].blank?
@search_term = params[:search] @search_term = params[:search]
@scope = params[:scope] @scope = search_service.scope
@show_snippets = params[:snippets].eql? 'true' @show_snippets = search_service.show_snippets?
@search_results = search_service.search_results
@search_results = @search_objects = search_service.search_objects
if @project
unless %w(blobs notes issues merge_requests milestones wiki_blobs
commits).include?(@scope)
@scope = 'blobs'
end
Search::ProjectService.new(@project, current_user, params).execute
elsif @show_snippets
unless %w(snippet_blobs snippet_titles).include?(@scope)
@scope = 'snippet_blobs'
end
Search::SnippetService.new(current_user, params).execute
else
unless %w(projects issues merge_requests milestones blobs commits).include?(@scope)
@scope = 'projects'
end
Search::GlobalService.new(current_user, params).execute
end
@search_objects = @search_results.objects(@scope, params[:page])
check_single_commit_result check_single_commit_result
end end
......
...@@ -80,5 +80,9 @@ module AuthHelper ...@@ -80,5 +80,9 @@ module AuthHelper
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current (current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
end end
def unlink_allowed?(provider)
%w(saml cas3).exclude?(provider.to_s)
end
extend self extend self
end end
...@@ -5,8 +5,8 @@ class BaseMailer < ActionMailer::Base ...@@ -5,8 +5,8 @@ class BaseMailer < ActionMailer::Base
attr_accessor :current_user attr_accessor :current_user
helper_method :current_user, :can? helper_method :current_user, :can?
default from: Proc.new { default_sender_address.format } default from: proc { default_sender_address.format }
default reply_to: Proc.new { default_reply_to_address.format } default reply_to: proc { default_reply_to_address.format }
def can? def can?
Ability.allowed?(current_user, action, subject) Ability.allowed?(current_user, action, subject)
......
...@@ -144,6 +144,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -144,6 +144,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :polling_interval_multiplier,
presence: true,
numericality: { greater_than_or_equal_to: 0 }
validates :minimum_mirror_sync_time, validates :minimum_mirror_sync_time,
presence: true, presence: true,
inclusion: { in: Gitlab::Mirror::SYNC_TIME_OPTIONS.values } inclusion: { in: Gitlab::Mirror::SYNC_TIME_OPTIONS.values }
...@@ -252,7 +256,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -252,7 +256,8 @@ class ApplicationSetting < ActiveRecord::Base
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0, terminal_max_session_time: 0,
two_factor_grace_period: 48, two_factor_grace_period: 48,
user_default_external: false user_default_external: false,
polling_interval_multiplier: 1
} }
end end
......
...@@ -164,11 +164,6 @@ module Ci ...@@ -164,11 +164,6 @@ module Ci
builds.latest.with_artifacts_not_expired.includes(project: [:namespace]) builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end end
# For now the only user who participates is the user who triggered
def participants(_current_user = nil)
Array(user)
end
def valid_commit_sha def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)") self.errors.add(:sha, " cant be 00000000 (branch removal)")
...@@ -210,7 +205,7 @@ module Ci ...@@ -210,7 +205,7 @@ module Ci
end end
def stuck? def stuck?
builds.pending.any?(&:stuck?) builds.pending.includes(:project).any?(&:stuck?)
end end
def retryable? def retryable?
......
module Ci module Ci
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
extend Ci::Model extend Ci::Model
prepend EE::Ci::Runner
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
LAST_CONTACT_TIME = 1.hour.ago LAST_CONTACT_TIME = 1.hour.ago
......
...@@ -3,4 +3,7 @@ module Importable ...@@ -3,4 +3,7 @@ module Importable
attr_accessor :importing attr_accessor :importing
alias_method :importing?, :importing alias_method :importing?, :importing
attr_accessor :imported
alias_method :imported?, :imported
end end
...@@ -14,6 +14,7 @@ module Issuable ...@@ -14,6 +14,7 @@ module Issuable
include Awardable include Awardable
include Taskable include Taskable
include TimeTrackable include TimeTrackable
include Importable
# This object is used to gather issuable meta data for displaying # This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests # upvotes, downvotes, notes and closing merge requests count for issues and merge requests
...@@ -102,7 +103,7 @@ module Issuable ...@@ -102,7 +103,7 @@ module Issuable
acts_as_paranoid acts_as_paranoid
after_save :update_assignee_cache_counts, if: :assignee_id_changed? after_save :update_assignee_cache_counts, if: :assignee_id_changed?
after_save :record_metrics after_save :record_metrics, unless: :imported?
def update_assignee_cache_counts def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist) # make sure we flush the cache for both the old *and* new assignees(if they exist)
......
...@@ -11,30 +11,20 @@ module RepositoryMirroring ...@@ -11,30 +11,20 @@ module RepositoryMirroring
gitlab_shell.delete_remote_branches(storage_path, path_with_namespace, remote, branches) gitlab_shell.delete_remote_branches(storage_path, path_with_namespace, remote, branches)
end end
def add_remote(name, url)
raw_repository.remote_add(name, url)
rescue Rugged::ConfigError
raw_repository.remote_update(name, url: url)
end
def remove_remote(name)
raw_repository.remote_delete(name)
true
rescue Rugged::ConfigError
false
end
def set_remote_as_mirror(name) def set_remote_as_mirror(name)
config = raw_repository.rugged.config config = raw_repository.rugged.config
# This is used by Gitlab Geo to define repository as equivalent as "git clone --mirror" # This is used to define repository as equivalent as "git clone --mirror"
config["remote.#{name}.fetch"] = 'refs/*:refs/*' config["remote.#{name}.fetch"] = 'refs/*:refs/*'
config["remote.#{name}.mirror"] = true config["remote.#{name}.mirror"] = true
config["remote.#{name}.prune"] = true config["remote.#{name}.prune"] = true
end end
def fetch_remote(remote, forced: false, no_tags: false) def fetch_mirror(remote, url)
gitlab_shell.fetch_remote(storage_path, path_with_namespace, remote, forced: forced, no_tags: no_tags) add_remote(remote, url)
set_remote_as_mirror(remote)
fetch_remote(remote, forced: true)
remove_remote(remote)
end end
def remote_tags(remote) def remote_tags(remote)
......
...@@ -6,8 +6,19 @@ module EE ...@@ -6,8 +6,19 @@ module EE
module Build module Build
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do
after_save :stick_build_if_status_changed
end
def shared_runners_minutes_limit_enabled? def shared_runners_minutes_limit_enabled?
runner && runner.shared? && project.shared_runners_minutes_limit_enabled? runner && runner.shared? && project.shared_runners_minutes_limit_enabled?
end end
def stick_build_if_status_changed
return unless status_changed?
return unless running?
::Gitlab::Database::LoadBalancing::Sticking.stick(:build, id)
end
end end
end end
module EE
module Ci
module Runner
def tick_runner_queue
::Gitlab::Database::LoadBalancing::Sticking.stick(:runner, token)
super
end
end
end
end
...@@ -4,7 +4,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -4,7 +4,6 @@ class MergeRequest < ActiveRecord::Base
include Referable include Referable
include Sortable include Sortable
include Elastic::MergeRequestsSearch include Elastic::MergeRequestsSearch
include Importable
include Approvable include Approvable
belongs_to :target_project, class_name: "Project" belongs_to :target_project, class_name: "Project"
......
...@@ -32,7 +32,7 @@ class Milestone < ActiveRecord::Base ...@@ -32,7 +32,7 @@ class Milestone < ActiveRecord::Base
validates :title, presence: true, uniqueness: { scope: :project_id } validates :title, presence: true, uniqueness: { scope: :project_id }
validates :project, presence: true validates :project, presence: true
validate :start_date_should_be_less_than_due_date, if: Proc.new { |m| m.start_date.present? && m.due_date.present? } validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
strip_attributes :title strip_attributes :title
......
...@@ -60,16 +60,25 @@ class NotificationSetting < ActiveRecord::Base ...@@ -60,16 +60,25 @@ class NotificationSetting < ActiveRecord::Base
def set_events def set_events
return if custom? return if custom?
EMAIL_EVENTS.each do |event| self.events = {}
events[event] = false
end
end end
# Validates store accessors values as boolean # Validates store accessors values as boolean
# It is a text field so it does not cast correct boolean values in JSON # It is a text field so it does not cast correct boolean values in JSON
def events_to_boolean def events_to_boolean
EMAIL_EVENTS.each do |event| EMAIL_EVENTS.each do |event|
events[event] = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(events[event]) bool = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(public_send(event))
events[event] = bool
end end
end end
# Allow people to receive failed pipeline notifications if they already have
# custom notifications enabled, as these are more like mentions than the other
# custom settings.
def failed_pipeline
bool = super
bool.nil? || bool
end
end end
...@@ -655,6 +655,10 @@ class Project < ActiveRecord::Base ...@@ -655,6 +655,10 @@ class Project < ActiveRecord::Base
import_type == 'gitea' import_type == 'gitea'
end end
def github_import?
import_type == 'github'
end
def check_limit def check_limit
unless creator.can_create_project? || namespace.kind == 'group' unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit projects_limit = creator.projects_limit
......
...@@ -22,22 +22,21 @@ class KubernetesService < DeploymentService ...@@ -22,22 +22,21 @@ class KubernetesService < DeploymentService
with_options presence: true, if: :activated? do with_options presence: true, if: :activated? do
validates :api_url, url: true validates :api_url, url: true
validates :token validates :token
validates :namespace,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message,
},
length: 1..63
end end
validates :namespace,
allow_blank: true,
length: 1..63,
if: :activated?,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
after_save :clear_reactive_cache! after_save :clear_reactive_cache!
def initialize_properties def initialize_properties
if properties.nil? self.properties = {} if properties.nil?
self.properties = {}
self.namespace = "#{project.path}-#{project.id}" if project.present?
end
end end
def title def title
...@@ -62,7 +61,7 @@ class KubernetesService < DeploymentService ...@@ -62,7 +61,7 @@ class KubernetesService < DeploymentService
{ type: 'text', { type: 'text',
name: 'namespace', name: 'namespace',
title: 'Kubernetes namespace', title: 'Kubernetes namespace',
placeholder: 'Kubernetes namespace' }, placeholder: namespace_placeholder },
{ type: 'text', { type: 'text',
name: 'api_url', name: 'api_url',
title: 'API URL', title: 'API URL',
...@@ -92,7 +91,7 @@ class KubernetesService < DeploymentService ...@@ -92,7 +91,7 @@ class KubernetesService < DeploymentService
variables = [ variables = [
{ key: 'KUBE_URL', value: api_url, public: true }, { key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false }, { key: 'KUBE_TOKEN', value: token, public: false },
{ key: 'KUBE_NAMESPACE', value: namespace, public: true } { key: 'KUBE_NAMESPACE', value: namespace_variable, public: true }
] ]
if ca_pem.present? if ca_pem.present?
...@@ -132,8 +131,26 @@ class KubernetesService < DeploymentService ...@@ -132,8 +131,26 @@ class KubernetesService < DeploymentService
{ pods: read_pods, deployments: read_deployments } { pods: read_pods, deployments: read_deployments }
end end
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
private private
def namespace_placeholder
default_namespace || TEMPLATE_PLACEHOLDER
end
def namespace_variable
if namespace.present?
namespace
else
default_namespace
end
end
def default_namespace
"#{project.path}-#{project.id}" if project.present?
end
def build_kubeclient!(api_path: 'api', api_version: 'v1') def build_kubeclient!(api_path: 'api', api_version: 'v1')
raise "Incomplete settings" unless api_url && namespace && token raise "Incomplete settings" unless api_url && namespace && token
......
...@@ -72,7 +72,7 @@ class Repository ...@@ -72,7 +72,7 @@ class Repository
# Return absolute path to repository # Return absolute path to repository
def path_to_repo def path_to_repo
@path_to_repo ||= File.expand_path( @path_to_repo ||= File.expand_path(
File.join(@project.repository_storage_path, path_with_namespace + ".git") File.join(repository_storage_path, path_with_namespace + ".git")
) )
end end
...@@ -409,10 +409,6 @@ class Repository ...@@ -409,10 +409,6 @@ class Repository
expire_tags_cache expire_tags_cache
end end
def before_import
expire_content_cache
end
# Runs code after the HEAD of a repository is changed. # Runs code after the HEAD of a repository is changed.
def after_change_head def after_change_head
expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys) expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
...@@ -1065,7 +1061,13 @@ class Repository ...@@ -1065,7 +1061,13 @@ class Repository
end end
def is_ancestor?(ancestor_id, descendant_id) def is_ancestor?(ancestor_id, descendant_id)
merge_base(ancestor_id, descendant_id) == ancestor_id Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
if is_enabled
raw_repository.is_ancestor?(ancestor_id, descendant_id)
else
merge_base_commit(ancestor_id, descendant_id) == ancestor_id
end
end
end end
def empty_repo? def empty_repo?
...@@ -1111,6 +1113,23 @@ class Repository ...@@ -1111,6 +1113,23 @@ class Repository
rugged.references.delete(tmp_ref) if tmp_ref rugged.references.delete(tmp_ref) if tmp_ref
end end
def add_remote(name, url)
raw_repository.remote_add(name, url)
rescue Rugged::ConfigError
raw_repository.remote_update(name, url: url)
end
def remove_remote(name)
raw_repository.remote_delete(name)
true
rescue Rugged::ConfigError
false
end
def fetch_remote(remote, forced: false, no_tags: false)
gitlab_shell.fetch_remote(repository_storage_path, path_with_namespace, remote, forced: forced, no_tags: no_tags)
end
def fetch_ref(source_path, source_ref, target_ref) def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo) Gitlab::Popen.popen(args, path_to_repo)
...@@ -1234,4 +1253,8 @@ class Repository ...@@ -1234,4 +1253,8 @@ class Repository
def repository_event(event, tags = {}) def repository_event(event, tags = {})
Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags)) Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
end end
def repository_storage_path
@project.repository_storage_path
end
end end
...@@ -25,7 +25,7 @@ class Service < ActiveRecord::Base ...@@ -25,7 +25,7 @@ class Service < ActiveRecord::Base
belongs_to :project, inverse_of: :services belongs_to :project, inverse_of: :services
has_one :service_hook has_one :service_hook
validates :project_id, presence: true, unless: Proc.new { |service| service.template? } validates :project_id, presence: true, unless: proc { |service| service.template? }
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') } scope :issue_trackers, -> { where(category: 'issue_tracker') }
......
class SystemNoteMetadata < ActiveRecord::Base class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[ ICON_TYPES = %w[
commit merge confidentiality status label assignee cross_reference commit merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved approvals title time_tracking branch milestone discussion task moved opened closed merged
approvals
].freeze ].freeze
validates :note, presence: true validates :note, presence: true
......
...@@ -169,6 +169,8 @@ class User < ActiveRecord::Base ...@@ -169,6 +169,8 @@ class User < ActiveRecord::Base
delegate :path, to: :namespace, allow_nil: true, prefix: true delegate :path, to: :namespace, allow_nil: true, prefix: true
accepts_nested_attributes_for :namespace
state_machine :state, initial: :active do state_machine :state, initial: :active do
event :block do event :block do
transition active: :blocked transition active: :blocked
......
...@@ -24,6 +24,10 @@ class BaseService ...@@ -24,6 +24,10 @@ class BaseService
Gitlab::AppLogger.info message Gitlab::AppLogger.info message
end end
def log_error(message)
Gitlab::AppLogger.error message
end
def system_hook_service def system_hook_service
SystemHooksService.new SystemHooksService.new
end end
......
...@@ -5,8 +5,6 @@ module Ci ...@@ -5,8 +5,6 @@ module Ci
def execute(pipeline) def execute(pipeline)
@pipeline = pipeline @pipeline = pipeline
ensure_created_builds! # TODO, remove me in 9.0
new_builds = new_builds =
stage_indexes_of_created_builds.map do |index| stage_indexes_of_created_builds.map do |index|
process_stage(index) process_stage(index)
...@@ -73,18 +71,5 @@ module Ci ...@@ -73,18 +71,5 @@ module Ci
def created_builds def created_builds
pipeline.builds.created pipeline.builds.created
end end
# This method is DEPRECATED and should be removed in 9.0.
#
# We need it to maintain backwards compatibility with previous versions
# when builds were not created within one transaction with the pipeline.
#
def ensure_created_builds!
return if created_builds.any?
Ci::CreatePipelineBuildsService
.new(project, current_user)
.execute(pipeline)
end
end end
end end
module EE
module UserProjectAccessChangedService
def execute
result = super
@user_ids.each do |id|
::Gitlab::Database::LoadBalancing::Sticking.stick(:user, id)
end
result
end
end
end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
# #
class NotificationRecipientService class NotificationRecipientService
attr_reader :project attr_reader :project
def initialize(project) def initialize(project)
@project = project @project = project
end end
...@@ -12,11 +12,7 @@ class NotificationRecipientService ...@@ -12,11 +12,7 @@ class NotificationRecipientService
custom_action = build_custom_key(action, target) custom_action = build_custom_key(action, target)
recipients = target.participants(current_user) recipients = target.participants(current_user)
recipients = add_project_watchers(recipients)
unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
recipients = add_project_watchers(recipients)
end
recipients = add_custom_notifications(recipients, custom_action) recipients = add_custom_notifications(recipients, custom_action)
recipients = reject_mention_users(recipients) recipients = reject_mention_users(recipients)
...@@ -43,6 +39,28 @@ class NotificationRecipientService ...@@ -43,6 +39,28 @@ class NotificationRecipientService
recipients.uniq recipients.uniq
end end
def build_pipeline_recipients(target, current_user, action:)
return [] unless current_user
custom_action =
case action.to_s
when 'failed'
:failed_pipeline
when 'success'
:success_pipeline
end
notification_setting = notification_setting_for_user_project(current_user, target.project)
return [] if notification_setting.mention? || notification_setting.disabled?
return [] if notification_setting.custom? && !notification_setting.public_send(custom_action)
return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
reject_users_without_access([current_user], target)
end
def build_relabeled_recipients(target, current_user, labels:) def build_relabeled_recipients(target, current_user, labels:)
recipients = add_labels_subscribers([], target, labels: labels) recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target) recipients = reject_unsubscribed_users(recipients, target)
...@@ -290,4 +308,16 @@ class NotificationRecipientService ...@@ -290,4 +308,16 @@ class NotificationRecipientService
def build_custom_key(action, object) def build_custom_key(action, object)
"#{action}_#{object.class.model_name.name.underscore}".to_sym "#{action}_#{object.class.model_name.name.underscore}".to_sym
end end
def notification_setting_for_user_project(user, project)
project_setting = user.notification_settings_for(project)
return project_setting unless project_setting.global?
group_setting = user.notification_settings_for(project.group)
return group_setting unless group_setting.global?
user.global_notification_setting
end
end end
...@@ -295,11 +295,11 @@ class NotificationService ...@@ -295,11 +295,11 @@ class NotificationService
return unless mailer.respond_to?(email_template) return unless mailer.respond_to?(email_template)
recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients( recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients(
pipeline, pipeline,
pipeline.user, pipeline.user,
action: pipeline.status, action: pipeline.status,
skip_current_user: false).map(&:notification_email) ).map(&:notification_email)
if recipients.any? if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later mailer.public_send(email_template, pipeline, recipients).deliver_later
......
...@@ -11,7 +11,7 @@ module Projects ...@@ -11,7 +11,7 @@ module Projects
success success
rescue => e rescue => e
error(e.message) error("Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}")
end end
private private
...@@ -32,23 +32,40 @@ module Projects ...@@ -32,23 +32,40 @@ module Projects
end end
def import_repository def import_repository
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
begin begin
raise Error, "Blocked import URL." if Gitlab::UrlBlocker.blocked_url?(project.import_url) if project.github_import? || project.gitea_import?
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url) fetch_repository
rescue => e else
clone_repository
end
rescue Gitlab::Shell::Error => e
# Expire cache to prevent scenarios such as: # Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true # 2. Retried import, repo is broken or not imported but +exists?+ still returns true
project.repository.before_import if project.repository_exists? project.repository.expire_content_cache if project.repository_exists?
raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" raise Error, e.message
end end
end end
def clone_repository
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
end
def fetch_repository
project.create_repository
project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true)
project.repository.remove_remote(project.import_type)
end
def import_data def import_data
return unless has_importer? return unless has_importer?
project.repository.before_import unless project.gitlab_project_import? project.repository.expire_content_cache unless project.gitlab_project_import?
unless importer.execute unless importer.execute
raise Error, 'The remote data could not be imported.' raise Error, 'The remote data could not be imported.'
......
...@@ -46,6 +46,7 @@ module Projects ...@@ -46,6 +46,7 @@ module Projects
end end
def error(message, http_status = nil) def error(message, http_status = nil)
log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest? @status.allow_failure = !latest?
@status.description = message @status.description = message
@status.drop @status.drop
......
...@@ -31,5 +31,14 @@ module Search ...@@ -31,5 +31,14 @@ module Search
Gitlab::SearchResults.new(current_user, projects, params[:search]) Gitlab::SearchResults.new(current_user, projects, params[:search])
end end
end end
def scope
@scope ||= begin
allowed_scopes = %w[issues merge_requests milestones]
allowed_scopes += %w[blobs commits] if current_application_settings.elasticsearch_search?
allowed_scopes.delete(params[:scope]) { 'projects' }
end
end
end end
end end
...@@ -21,5 +21,9 @@ module Search ...@@ -21,5 +21,9 @@ module Search
params[:repository_ref]) params[:repository_ref])
end end
end end
def scope
@scope ||= %w[notes issues merge_requests milestones wiki_blobs commits].delete(params[:scope]) { 'blobs' }
end
end end
end end
...@@ -16,5 +16,9 @@ module Search ...@@ -16,5 +16,9 @@ module Search
Gitlab::SnippetSearchResults.new(snippets, params[:search]) Gitlab::SnippetSearchResults.new(snippets, params[:search])
end end
end end
def scope
@scope ||= %w[snippet_titles].delete(params[:scope]) { 'snippet_blobs' }
end
end end
end end
class SearchService
include Gitlab::Allowable
def initialize(current_user, params = {})
@current_user = current_user
@params = params.dup
end
def project
return @project if defined?(@project)
@project =
if params[:project_id].present?
the_project = Project.find_by(id: params[:project_id])
can?(current_user, :download_code, the_project) ? the_project : nil
else
nil
end
end
def group
return @group if defined?(@group)
@group =
if params[:group_id].present?
the_group = Group.find_by(id: params[:group_id])
can?(current_user, :read_group, the_group) ? the_group : nil
else
nil
end
end
def show_snippets?
return @show_snippets if defined?(@show_snippets)
@show_snippets = params[:snippets] == 'true'
end
delegate :scope, to: :search_service
def search_results
@search_results ||= search_service.execute
end
def search_objects
@search_objects ||= search_results.objects(scope, params[:page])
end
private
def search_service
@search_service ||=
if project
Search::ProjectService.new(project, current_user, params)
elsif show_snippets?
Search::SnippetService.new(current_user, params)
else
Search::GlobalService.new(current_user, params)
end
end
attr_reader :current_user, :params
end
...@@ -183,7 +183,9 @@ module SystemNoteService ...@@ -183,7 +183,9 @@ module SystemNoteService
body = status.dup body = status.dup
body << " via #{source.gfm_reference(project)}" if source body << " via #{source.gfm_reference(project)}" if source
create_note(NoteSummary.new(noteable, project, author, body, action: 'status')) action = status == 'reopened' ? 'opened' : status
create_note(NoteSummary.new(noteable, project, author, body, action: action))
end end
# Called when 'merge when pipeline succeeds' is executed # Called when 'merge when pipeline succeeds' is executed
...@@ -273,9 +275,15 @@ module SystemNoteService ...@@ -273,9 +275,15 @@ module SystemNoteService
# #
# Returns the created Note object # Returns the created Note object
def change_issue_confidentiality(issue, project, author) def change_issue_confidentiality(issue, project, author)
body = issue.confidential ? 'made the issue confidential' : 'made the issue visible to everyone' if issue.confidential
body = 'made the issue confidential'
action = 'confidential'
else
body = 'made the issue visible to everyone'
action = 'visible'
end
create_note(NoteSummary.new(issue, project, author, body, action: 'confidentiality')) create_note(NoteSummary.new(issue, project, author, body, action: action))
end end
# Called when a branch in Noteable is changed # Called when a branch in Noteable is changed
......
...@@ -212,7 +212,7 @@ class TodoService ...@@ -212,7 +212,7 @@ class TodoService
# Only update those that are not really on that state # Only update those that are not really on that state
todos = todos.where.not(state: state) todos = todos.where.not(state: state)
todos_ids = todos.pluck(:id) todos_ids = todos.pluck(:id)
todos.update_all(state: state) todos.unscope(:order).update_all(state: state)
current_user.update_todos_count_cache current_user.update_todos_count_cache
todos_ids todos_ids
end end
......
class UserProjectAccessChangedService class UserProjectAccessChangedService
prepend EE::UserProjectAccessChangedService
def initialize(user_ids) def initialize(user_ids)
@user_ids = Array.wrap(user_ids) @user_ids = Array.wrap(user_ids)
end end
......
...@@ -20,10 +20,10 @@ module Users ...@@ -20,10 +20,10 @@ module Users
Groups::DestroyService.new(group, current_user).execute Groups::DestroyService.new(group, current_user).execute
end end
user.personal_projects.each do |project| user.personal_projects.with_deleted.each do |project|
# Skip repository removal because we remove directory with namespace # Skip repository removal because we remove directory with namespace
# that contain all this repositories # that contain all this repositories
::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end end
move_issues_to_ghost_user(user) move_issues_to_ghost_user(user)
......
...@@ -642,6 +642,20 @@ ...@@ -642,6 +642,20 @@
Maximum time for web terminal websocket connection (in seconds). Maximum time for web terminal websocket connection (in seconds).
0 for unlimited. 0 for unlimited.
%fieldset
%legend Real-time features
.form-group
= f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :polling_interval_multiplier, class: 'form-control'
.help-block
Change this value to influence how frequently the GitLab UI polls for updates.
If you set the value to 2 all polling intervals are multiplied
by 2, which means that polling happens half as frequently.
The multiplier can also have a decimal value.
The default value (1) is a reasonable choice for the majority of GitLab
installations. Set to 0 to completely disable polling.
- if Gitlab::Geo.license_allows? - if Gitlab::Geo.license_allows?
%fieldset %fieldset
%legend GitLab Geo %legend GitLab Geo
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= render 'groups/group_lfs_settings', f: f = render 'groups/group_lfs_settings', f: f
= render 'groups/shared_runners_minutes_setting', f: f = render 'namespaces/shared_runners_minutes_setting', f: f
- if @group.new_record? - if @group.new_record?
.form-group .form-group
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
= group_lfs_status(@group) = group_lfs_status(@group)
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
= render "shared_runner_status", group: @group = render partial: "namespaces/shared_runner_status", locals: { namespace: @group }
.panel.panel-default .panel.panel-default
.panel-heading Linked LDAP groups .panel-heading Linked LDAP groups
......
...@@ -42,6 +42,8 @@ ...@@ -42,6 +42,8 @@
= render partial: 'access_levels', locals: { f: f } = render partial: 'access_levels', locals: { f: f }
= render partial: 'limits', locals: { f: f }
%fieldset %fieldset
%legend Profile %legend Profile
.form-group .form-group
......
= f.fields_for :namespace do |namespace_form|
= namespace_form.hidden_field :id
%fieldset
%legend Limits
= render "namespaces/shared_runners_minutes_setting", f: namespace_form
...@@ -123,6 +123,8 @@ ...@@ -123,6 +123,8 @@
%strong %strong
= link_to @user.created_by.name, [:admin, @user.created_by] = link_to @user.created_by.name, [:admin, @user.created_by]
= render partial: "namespaces/shared_runner_status", locals: { namespace: @user.namespace }
.col-md-6 .col-md-6
- unless @user == current_user - unless @user == current_user
- unless @user.confirmed? - unless @user.confirmed?
......
...@@ -35,6 +35,15 @@ ...@@ -35,6 +35,15 @@
%li %li
= link_to admin_root_path, title: 'Admin Area', aria: { label: "Admin Area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to admin_root_path, title: 'Admin Area', aria: { label: "Admin Area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw') = icon('wrench fw')
- if current_user.can_create_project?
%li
= link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('plus fw')
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
%li %li
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('hashtag fw') = icon('hashtag fw')
...@@ -50,10 +59,6 @@ ...@@ -50,10 +59,6 @@
= icon('check-circle fw') = icon('check-circle fw')
%span.badge.todos-count %span.badge.todos-count
= todos_count_format(todos_pending_count) = todos_count_format(todos_pending_count)
- if current_user.can_create_project?
%li
= link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('plus fw')
- if Gitlab::Geo.secondary? - if Gitlab::Geo.secondary?
%li %li
...@@ -65,6 +70,7 @@ ...@@ -65,6 +70,7 @@
= link_to sherlock_transactions_path, title: 'Sherlock Transactions', = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw') = icon('tachometer fw')
%li.header-user.dropdown %li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
......
- if group.shared_runners_enabled? - namespace = local_assigns.fetch(:namespace)
- if namespace.shared_runners_enabled?
%li %li
%span.light Build minutes quota: %span.light Build minutes quota:
%strong %strong
= group_shared_runner_limits_quota(group) = group_shared_runner_limits_quota(namespace)
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-build-minutes-quota"), target: '_blank' = link_to icon('question-circle'), help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-build-minutes-quota"), target: '_blank'
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Project #{@project.name} was exported successfully. Project #{@project.name} was exported successfully.
%p %p
The project export can be downloaded from: The project export can be downloaded from:
= link_to download_export_namespace_project_url(@project.namespace, @project) do = link_to download_export_namespace_project_url(@project.namespace, @project), rel: 'nofollow', download: '', do
= @project.name_with_namespace + " export" = @project.name_with_namespace + " export"
%p %p
The download link will expire in 24 hours. The download link will expire in 24 hours.
...@@ -75,12 +75,12 @@ ...@@ -75,12 +75,12 @@
.provider-btn-image .provider-btn-image
= provider_image_tag(provider) = provider_image_tag(provider)
- if auth_active?(provider) - if auth_active?(provider)
- if provider.to_s == 'saml' - if unlink_allowed?(provider)
%a.provider-btn
Active
- else
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
Disconnect Disconnect
- else
%a.provider-btn
Active
- else - else
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do
Connect Connect
......
- empty_repo = @project.empty_repo? - empty_repo = @project.empty_repo?
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) } .project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
%div{ class: container_class } .limit-container-width{ class: container_class }
.avatar-container.s70.project-avatar .avatar-container.s70.project-avatar
= project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile') = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile')
%h1.project-title %h1.project-title
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.top-block.row-content-block.clearfix .top-block.row-content-block.clearfix
.pull-right .pull-right
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
class: 'btn btn-default download' do rel: 'nofollow', download: '', class: 'btn btn-default download' do
= icon('download') = icon('download')
Download artifacts archive Download artifacts archive
......
- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create'
.file-holder.file.append-bottom-default .file-holder.file.append-bottom-default
.js-file-title.file-title.clearfix .js-file-title.file-title.clearfix{ data: { current_action: action } }
.editor-ref .editor-ref
= icon('code-fork') = icon('code-fork')
= ref = ref
%span.editor-file-name %span.editor-file-name
- if current_action?(:edit) || current_action?(:update) - if current_action?(:edit) || current_action?(:update)
= text_field_tag 'file_path', (params[:file_path] || @path), = text_field_tag 'file_path', (params[:file_path] || @path),
class: 'form-control new-file-path' class: 'form-control new-file-path js-file-path-name-input'
- if current_action?(:new) || current_action?(:create) - if current_action?(:new) || current_action?(:create)
%span.editor-file-name %span.editor-file-name
\/ \/
= text_field_tag 'file_name', params[:file_name], placeholder: "File name", = text_field_tag 'file_name', params[:file_name], placeholder: "File name",
required: true, class: 'form-control new-file-name' required: true, class: 'form-control new-file-name js-file-path-name-input'
.pull-right.file-buttons .pull-right.file-buttons
.license-selector.js-license-selector-wrap.hidden = button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do
= dropdown_tag("Choose a License template", options: { toggle_class: 'btn js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.hidden
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
= dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.dockerfile-selector.js-dockerfile-selector-wrap.hidden
= dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
= button_tag class: 'soft-wrap-toggle btn', type: 'button' do
%span.no-wrap %span.no-wrap
= custom_icon('icon_no_wrap') = custom_icon('icon_no_wrap')
No wrap No wrap
...@@ -31,7 +25,7 @@ ...@@ -31,7 +25,7 @@
= custom_icon('icon_soft_wrap') = custom_icon('icon_soft_wrap')
Soft wrap Soft wrap
.encoding-selector .encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1'
.file-editor.code .file-editor.code
%pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data] %pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data]
......
.template-selectors-menu
.templates-selectors-label
Template
.template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden
= dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } )
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a License template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
.template-selectors-undo-menu.hidden
%span.text-info Template applied
%button.btn.btn-sm.btn-info Undo
...@@ -11,12 +11,15 @@ ...@@ -11,12 +11,15 @@
Someone edited the file the same time you did. Please check out Someone edited the file the same time you did. Please check out
= link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer' = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs. and make sure your changes will not unintentionally remove theirs.
.editor-title-row
%h3.page-title.blob-edit-page-title
Edit file
= render 'template_selectors'
.file-editor .file-editor
%ul.nav-links.no-bottom.js-edit-mode %ul.nav-links.no-bottom.js-edit-mode
%li.active %li.active
= link_to '#editor' do = link_to '#editor' do
Edit File Write
%li %li
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js') = page_specific_javascript_tag('lib/ace.js')
= page_specific_javascript_bundle_tag('blob') = page_specific_javascript_bundle_tag('blob')
.editor-title-row
%h3.page-title %h3.page-title.blob-new-page-title
New File New file
= render 'template_selectors'
.file-editor .file-editor
= form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref = render 'projects/blob/editor', ref: @ref
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
= link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep Keep
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
Download Download
- if @build.artifacts_metadata? - if @build.artifacts_metadata?
......
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
%td %td
.pull-right .pull-right
- if can?(current_user, :read_build, build) && build.artifacts? - if can?(current_user, :read_build, build) && build.artifacts?
= link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
= icon('download') = icon('download')
- if can?(current_user, :update_build, build) - if can?(current_user, :update_build, build)
- if build.active? - if build.active?
......
...@@ -174,7 +174,7 @@ ...@@ -174,7 +174,7 @@
- if @project.export_project_path - if @project.export_project_path
= link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project), = link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project),
method: :get, class: "btn btn-default" rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_namespace_project_path(@project.namespace, @project), = link_to 'Generate new export', generate_new_export_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-default" method: :post, class: "btn btn-default"
- else - else
...@@ -249,6 +249,8 @@ ...@@ -249,6 +249,8 @@
%ul %ul
%li Be careful. Renaming a project's repository can have unintended side effects. %li Be careful. Renaming a project's repository can have unintended side effects.
%li You will need to update your local repositories to point to the new location. %li You will need to update your local repositories to point to the new location.
- if @project.deployment_services.any?
%li Your deployment services will be broken, you will need to manually fix the services after renaming.
= f.submit 'Rename project', class: "btn btn-warning" = f.submit 'Rename project', class: "btn btn-warning"
- if can?(current_user, :change_namespace, @project) - if can?(current_user, :change_namespace, @project)
%hr %hr
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
= render "home_panel" = render "home_panel"
- if current_user && can?(current_user, :download_code, @project) - if current_user && can?(current_user, :download_code, @project)
%nav.project-stats{ class: container_class } %nav.project-stats.limit-container-width{ class: container_class }
%ul.nav %ul.nav
%li %li
= link_to project_files_path(@project) do = link_to project_files_path(@project) do
...@@ -77,11 +77,11 @@ ...@@ -77,11 +77,11 @@
Set up auto deploy Set up auto deploy
- if @repository.commit - if @repository.commit
%div{ class: container_class } .limit-container-width{ class: container_class }
.project-last-commit .project-last-commit
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
%div{ class: container_class } .limit-container-width{ class: container_class }
- if @project.archived? - if @project.archived?
.text-warning.center.prepend-top-20 .text-warning.center.prepend-top-20
%p %p
......
...@@ -20,9 +20,9 @@ ...@@ -20,9 +20,9 @@
= link_to page_filter_path(sort: sort_value_oldest_updated, label: true) do = link_to page_filter_path(sort: sort_value_oldest_updated, label: true) do
= sort_title_oldest_updated = sort_title_oldest_updated
- if local_assigns[:type] == :issues - if local_assigns[:type] == :issues
= link_to page_filter_path(sort: sort_value_more_weight) do = link_to page_filter_path(sort: sort_value_more_weight, label: true) do
= sort_title_more_weight = sort_title_more_weight
= link_to page_filter_path(sort: sort_value_less_weight) do = link_to page_filter_path(sort: sort_value_less_weight, label: true) do
= sort_title_less_weight = sort_title_less_weight
= link_to page_filter_path(sort: sort_value_milestone_soon, label: true) do = link_to page_filter_path(sort: sort_value_milestone_soon, label: true) do
= sort_title_milestone_soon = sort_title_milestone_soon
......
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.
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.
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