Commit 89d4144c authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-02-08

# Conflicts:
#	app/assets/javascripts/pipelines/components/async_button.vue
#	locale/gitlab.pot

[ci skip]
parents 2495bbbd 07e1bcc0
...@@ -306,7 +306,7 @@ group :metrics do ...@@ -306,7 +306,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~> 0.7.0.beta44' gem 'prometheus-client-mmap', '~> 0.9.1'
gem 'raindrops', '~> 0.18' gem 'raindrops', '~> 0.18'
end end
...@@ -426,7 +426,7 @@ group :ed25519 do ...@@ -426,7 +426,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.83.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.84.0', require: 'gitaly'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1' gem 'google-protobuf', '= 3.5.1'
......
...@@ -309,7 +309,7 @@ GEM ...@@ -309,7 +309,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.83.0) gitaly-proto (0.84.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)
...@@ -665,7 +665,7 @@ GEM ...@@ -665,7 +665,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.7.0.beta44) prometheus-client-mmap (0.9.1)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
...@@ -1091,7 +1091,7 @@ DEPENDENCIES ...@@ -1091,7 +1091,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.83.0) gitaly-proto (~> 0.84.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
...@@ -1170,7 +1170,7 @@ DEPENDENCIES ...@@ -1170,7 +1170,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3) peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2) pg (~> 0.18.2)
premailer-rails (~> 1.9.7) premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta44) prometheus-client-mmap (~> 0.9.1)
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
......
...@@ -133,6 +133,21 @@ var Dispatcher; ...@@ -133,6 +133,21 @@ var Dispatcher;
.then(callDefault) .then(callDefault)
.catch(fail); .catch(fail);
break; break;
case 'admin:projects:index':
import('./pages/admin/projects/index/index')
.then(callDefault)
.catch(fail);
break;
case 'admin:users:index':
import('./pages/admin/users/shared')
.then(callDefault)
.catch(fail);
break;
case 'admin:users:show':
import('./pages/admin/users/shared')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:projects:index': case 'dashboard:projects:index':
case 'dashboard:projects:starred': case 'dashboard:projects:starred':
import('./pages/dashboard/projects') import('./pages/dashboard/projects')
......
...@@ -152,14 +152,14 @@ export default { ...@@ -152,14 +152,14 @@ export default {
showLeaveGroupModal(group, parentGroup) { showLeaveGroupModal(group, parentGroup) {
this.targetGroup = group; this.targetGroup = group;
this.targetParentGroup = parentGroup; this.targetParentGroup = parentGroup;
this.showModal = true; this.updateModal = true;
this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`); this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`);
}, },
hideLeaveGroupModal() { hideLeaveGroupModal() {
this.showModal = false; this.updateModal = false;
}, },
leaveGroup() { leaveGroup() {
this.showModal = false; this.updateModal = false;
this.targetGroup.isBeingRemoved = true; this.targetGroup.isBeingRemoved = true;
this.service.leaveGroup(this.targetGroup.leavePath) this.service.leaveGroup(this.targetGroup.leavePath)
.then(res => res.json()) .then(res => res.json())
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
/* global ace */ /* global ace */
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../flash'; import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
((global) => { ((global) => {
global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts = global.mergeConflicts || {};
...@@ -49,27 +51,26 @@ import Flash from '../../flash'; ...@@ -49,27 +51,26 @@ import Flash from '../../flash';
loadEditor() { loadEditor() {
this.loading = true; this.loading = true;
$.get(this.file.content_path) axios.get(this.file.content_path)
.done((file) => { .then(({ data }) => {
const content = this.$el.querySelector('pre'); const content = this.$el.querySelector('pre');
const fileContent = document.createTextNode(file.content); const fileContent = document.createTextNode(data.content);
content.textContent = fileContent.textContent; content.textContent = fileContent.textContent;
this.originalContent = file.content; this.originalContent = data.content;
this.fileLoaded = true; this.fileLoaded = true;
this.editor = ace.edit(content); this.editor = ace.edit(content);
this.editor.$blockScrolling = Infinity; // Turn off annoying warning this.editor.$blockScrolling = Infinity; // Turn off annoying warning
this.editor.getSession().setMode(`ace/mode/${file.blob_ace_mode}`); this.editor.getSession().setMode(`ace/mode/${data.blob_ace_mode}`);
this.editor.on('change', () => { this.editor.on('change', () => {
this.saveDiffResolution(); this.saveDiffResolution();
}); });
this.saveDiffResolution(); this.saveDiffResolution();
this.loading = false;
}) })
.fail(() => { .catch(() => {
new Flash('Failed to load the file, please try again.'); flash(__('An error occurred while loading the file'));
})
.always(() => {
this.loading = false; this.loading = false;
}); });
}, },
......
...@@ -102,6 +102,7 @@ ...@@ -102,6 +102,7 @@
.then(() => { .then(() => {
this.isEditing = false; this.isEditing = false;
this.isRequesting = false; this.isRequesting = false;
this.oldContent = null;
$(this.$refs.noteBody.$el).renderGFM(); $(this.$refs.noteBody.$el).renderGFM();
this.$refs.noteBody.resetAutoSave(); this.$refs.noteBody.resetAutoSave();
callback(); callback();
......
<script>
import _ from 'underscore';
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
export default {
components: {
modal,
},
props: {
deleteProjectUrl: {
type: String,
required: false,
default: '',
},
projectName: {
type: String,
required: false,
default: '',
},
csrfToken: {
type: String,
required: false,
default: '',
},
},
data() {
return {
enteredProjectName: '',
};
},
computed: {
title() {
return sprintf(s__('AdminProjects|Delete Project %{projectName}?'),
{
projectName: `'${_.escape(this.projectName)}'`,
},
false,
);
},
text() {
return sprintf(s__(`AdminProjects|
You’re about to permanently delete the project %{projectName}, its repository,
and all related resources including issues, merge requests, etc.. Once you confirm and press
%{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`),
{
projectName: `<strong>${_.escape(this.projectName)}</strong>`,
strong_start: '<strong>',
strong_end: '</strong>',
},
false,
);
},
confirmationTextLabel() {
return sprintf(s__('AdminUsers|To confirm, type %{projectName}'),
{
projectName: `<code>${_.escape(this.projectName)}</code>`,
},
false,
);
},
primaryButtonLabel() {
return s__('AdminProjects|Delete project');
},
canSubmit() {
return this.enteredProjectName === this.projectName;
},
},
methods: {
onCancel() {
this.enteredProjectName = '';
},
onSubmit() {
this.$refs.form.submit();
this.enteredProjectName = '';
},
},
};
</script>
<template>
<modal
id="delete-project-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
:submit-disabled="!canSubmit"
@submit="onSubmit"
@cancel="onCancel"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
<p v-html="confirmationTextLabel"></p>
<form
ref="form"
:action="deleteProjectUrl"
method="post"
>
<input
ref="method"
type="hidden"
name="_method"
value="delete"
/>
<input
type="hidden"
name="authenticity_token"
:value="csrfToken"
/>
<input
name="projectName"
class="form-control"
type="text"
v-model="enteredProjectName"
aria-labelledby="input-label"
autocomplete="off"
/>
</form>
</template>
</modal>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import csrf from '~/lib/utils/csrf';
import deleteProjectModal from './components/delete_project_modal.vue';
export default () => {
Vue.use(Translate);
const deleteProjectModalEl = document.getElementById('delete-project-modal');
const deleteModal = new Vue({
el: deleteProjectModalEl,
data: {
deleteProjectUrl: '',
projectName: '',
},
render(createElement) {
return createElement(deleteProjectModal, {
props: {
deleteProjectUrl: this.deleteProjectUrl,
projectName: this.projectName,
csrfToken: csrf.token,
},
});
},
});
$(document).on('shown.bs.modal', (event) => {
if (event.relatedTarget.classList.contains('delete-project-button')) {
const buttonProps = event.relatedTarget.dataset;
deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl;
deleteModal.projectName = buttonProps.projectName;
}
});
};
<script>
import _ from 'underscore';
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
export default {
components: {
modal,
},
props: {
deleteUserUrl: {
type: String,
required: false,
default: '',
},
blockUserUrl: {
type: String,
required: false,
default: '',
},
deleteContributions: {
type: Boolean,
required: false,
default: false,
},
username: {
type: String,
required: false,
default: '',
},
csrfToken: {
type: String,
required: false,
default: '',
},
},
data() {
return {
enteredUsername: '',
};
},
computed: {
title() {
const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?');
const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?');
return sprintf(
this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, {
username: `'${_.escape(this.username)}'`,
}, false);
},
text() {
const keepContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
This will delete all of the issues, merge requests, and groups linked to them.
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
const deleteContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
{
username: `<strong>${_.escape(this.username)}</strong>`,
strong_start: '<strong>',
strong_end: '</strong>',
},
false,
);
},
confirmationTextLabel() {
return sprintf(s__('AdminUsers|To confirm, type %{username}'),
{
username: `<code>${_.escape(this.username)}</code>`,
},
false,
);
},
primaryButtonLabel() {
const keepContributionsLabel = s__('AdminUsers|Delete user');
const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions');
return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel;
},
secondaryButtonLabel() {
return s__('AdminUsers|Block user');
},
canSubmit() {
return this.enteredUsername === this.username;
},
},
methods: {
onCancel() {
this.enteredUsername = '';
},
onSecondaryAction() {
const form = this.$refs.form;
form.action = this.blockUserUrl;
this.$refs.method.value = 'put';
form.submit();
},
onSubmit() {
this.$refs.form.submit();
this.enteredUsername = '';
},
},
};
</script>
<template>
<modal
id="delete-user-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
:secondary-button-label="secondaryButtonLabel"
:submit-disabled="!canSubmit"
@submit="onSubmit"
@cancel="onCancel"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
<p v-html="confirmationTextLabel"></p>
<form
ref="form"
:action="deleteUserUrl"
method="post"
>
<input
ref="method"
type="hidden"
name="_method"
value="delete"
/>
<input
type="hidden"
name="authenticity_token"
:value="csrfToken"
/>
<input
type="text"
name="username"
class="form-control"
v-model="enteredUsername"
aria-labelledby="input-label"
autocomplete="off"
/>
</form>
</template>
<template
slot="secondary-button"
slot-scope="props"
>
<button
type="button"
class="btn js-secondary-button btn-warning"
:disabled="!canSubmit"
@click="onSecondaryAction"
data-dismiss="modal"
>
{{ secondaryButtonLabel }}
</button>
</template>
</modal>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import csrf from '~/lib/utils/csrf';
import deleteUserModal from './components/delete_user_modal.vue';
export default () => {
Vue.use(Translate);
const deleteUserModalEl = document.getElementById('delete-user-modal');
const deleteModal = new Vue({
el: deleteUserModalEl,
data: {
deleteUserUrl: '',
blockUserUrl: '',
deleteContributions: '',
username: '',
},
render(createElement) {
return createElement(deleteUserModal, {
props: {
deleteUserUrl: this.deleteUserUrl,
blockUserUrl: this.blockUserUrl,
deleteContributions: this.deleteContributions,
username: this.username,
csrfToken: csrf.token,
},
});
},
});
$(document).on('shown.bs.modal', (event) => {
if (event.relatedTarget.classList.contains('delete-user-button')) {
const buttonProps = event.relatedTarget.dataset;
deleteModal.deleteUserUrl = buttonProps.deleteUserUrl;
deleteModal.blockUserUrl = buttonProps.blockUserUrl;
deleteModal.deleteContributions = event.relatedTarget.hasAttribute('data-delete-contributions');
deleteModal.username = buttonProps.username;
}
});
};
...@@ -32,9 +32,15 @@ ...@@ -32,9 +32,15 @@
type: String, type: String,
required: true, required: true,
}, },
<<<<<<< HEAD
confirmActionMessage: { confirmActionMessage: {
type: String, type: String,
required: false, required: false,
=======
id: {
type: Number,
required: true,
>>>>>>> upstream/master
}, },
}, },
...@@ -50,11 +56,18 @@ ...@@ -50,11 +56,18 @@
}, },
methods: { methods: {
onClick() { onClick() {
<<<<<<< HEAD
if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
this.makeRequest(); this.makeRequest();
} else if (!this.confirmActionMessage) { } else if (!this.confirmActionMessage) {
this.makeRequest(); this.makeRequest();
} }
=======
eventHub.$emit('actionConfirmationModal', {
id: this.id,
callback: this.makeRequest,
});
>>>>>>> upstream/master
}, },
makeRequest() { makeRequest() {
this.isLoading = true; this.isLoading = true;
......
<script> <script>
import pipelinesTableRowComponent from './pipelines_table_row.vue'; import pipelinesTableRowComponent from './pipelines_table_row.vue';
import stopConfirmationModal from './stop_confirmation_modal.vue';
import retryConfirmationModal from './retry_confirmation_modal.vue';
/** /**
* Pipelines Table Component. * Pipelines Table Component.
...@@ -9,6 +11,8 @@ ...@@ -9,6 +11,8 @@
export default { export default {
components: { components: {
pipelinesTableRowComponent, pipelinesTableRowComponent,
stopConfirmationModal,
retryConfirmationModal,
}, },
props: { props: {
pipelines: { pipelines: {
...@@ -70,5 +74,7 @@ ...@@ -70,5 +74,7 @@
:auto-devops-help-path="autoDevopsHelpPath" :auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType" :view-type="viewType"
/> />
<stop-confirmation-modal />
<retry-confirmation-modal />
</div> </div>
</template> </template>
...@@ -306,6 +306,9 @@ ...@@ -306,6 +306,9 @@
css-class="js-pipelines-retry-button btn-default btn-retry" css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry" title="Retry"
icon="repeat" icon="repeat"
:id="pipeline.id"
data-toggle="modal"
data-target="#retry-confirmation-modal"
/> />
<async-button-component <async-button-component
...@@ -314,7 +317,9 @@ ...@@ -314,7 +317,9 @@
css-class="js-pipelines-cancel-button btn-remove" css-class="js-pipelines-cancel-button btn-remove"
title="Cancel" title="Cancel"
icon="close" icon="close"
confirm-action-message="Are you sure you want to cancel this pipeline?" :id="pipeline.id"
data-toggle="modal"
data-target="#stop-confirmation-modal"
/> />
</div> </div>
</div> </div>
......
<script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
modal,
},
data() {
return {
id: '',
callback: () => {},
};
},
computed: {
title() {
return sprintf(s__('Pipeline|Retry pipeline #%{id}?'), {
id: `'${this.id}'`,
}, false);
},
text() {
return sprintf(s__('Pipeline|You’re about to retry pipeline %{id}.'), {
id: `<strong>#${this.id}</strong>`,
}, false);
},
primaryButtonLabel() {
return s__('Pipeline|Retry pipeline');
},
},
created() {
eventHub.$on('actionConfirmationModal', this.updateModal);
},
beforeDestroy() {
eventHub.$off('actionConfirmationModal', this.updateModal);
},
methods: {
updateModal(action) {
this.id = action.id;
this.callback = action.callback;
},
onSubmit() {
this.callback();
},
},
};
</script>
<template>
<modal
id="retry-confirmation-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</template>
<script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
modal,
},
data() {
return {
id: '',
callback: () => {},
};
},
computed: {
title() {
return sprintf(s__('Pipeline|Stop pipeline #%{id}?'), {
id: `'${this.id}'`,
}, false);
},
text() {
return sprintf(s__('Pipeline|You’re about to stop pipeline %{id}.'), {
id: `<strong>#${this.id}</strong>`,
}, false);
},
primaryButtonLabel() {
return s__('Pipeline|Stop pipeline');
},
},
created() {
eventHub.$on('actionConfirmationModal', this.updateModal);
},
beforeDestroy() {
eventHub.$off('actionConfirmationModal', this.updateModal);
},
methods: {
updateModal(action) {
this.id = action.id;
this.callback = action.callback;
},
onSubmit() {
this.callback();
},
},
};
</script>
<template>
<modal
id="stop-confirmation-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</template>
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ /* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Flash from '../flash'; import { getPagePath } from '~/lib/utils/common_utils';
import { getPagePath } from '../lib/utils/common_utils'; import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import flash from '../flash';
((global) => { ((global) => {
class Profile { class Profile {
...@@ -57,8 +59,8 @@ import { getPagePath } from '../lib/utils/common_utils'; ...@@ -57,8 +59,8 @@ import { getPagePath } from '../lib/utils/common_utils';
onUpdateNotifs(e, data) { onUpdateNotifs(e, data) {
return data.saved ? return data.saved ?
new Flash("Notification settings saved", "notice") : flash(__('Notification settings saved'), 'notice') :
new Flash("Failed to save new settings", "alert"); flash(__('Failed to save new settings'));
} }
saveForm() { saveForm() {
...@@ -70,21 +72,18 @@ import { getPagePath } from '../lib/utils/common_utils'; ...@@ -70,21 +72,18 @@ import { getPagePath } from '../lib/utils/common_utils';
formData.append('user[avatar]', avatarBlob, 'avatar.png'); formData.append('user[avatar]', avatarBlob, 'avatar.png');
} }
return $.ajax({ axios({
method: this.form.attr('method'),
url: this.form.attr('action'), url: this.form.attr('action'),
type: this.form.attr('method'),
data: formData, data: formData,
dataType: "json", })
processData: false, .then(({ data }) => flash(data.message, 'notice'))
contentType: false, .then(() => {
success: response => new Flash(response.message, 'notice'), window.scrollTo(0, 0);
error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'), // Enable submit button after requests ends
complete: () => { self.form.find(':input[disabled]').enable();
window.scrollTo(0, 0); })
// Enable submit button after requests ends .catch(error => flash(error.message));
return self.form.find(':input[disabled]').enable();
}
});
} }
setNewRepoCookie() { setNewRepoCookie() {
......
...@@ -46,6 +46,11 @@ ...@@ -46,6 +46,11 @@
required: false, required: false,
default: '', default: '',
}, },
secondaryButtonLabel: {
type: String,
required: false,
default: '',
},
submitDisabled: { submitDisabled: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -129,6 +134,21 @@ ...@@ -129,6 +134,21 @@
> >
{{ closeButtonLabel }} {{ closeButtonLabel }}
</button> </button>
<slot
v-if="secondaryButtonLabel"
name="secondary-button"
>
<button
v-if="secondaryButtonLabel"
type="button"
class="btn"
data-dismiss="modal"
>
{{ secondaryButtonLabel }}
</button>
</slot>
<button <button
v-if="primaryButtonLabel" v-if="primaryButtonLabel"
type="button" type="button"
......
...@@ -13,10 +13,7 @@ class RootController < Dashboard::ProjectsController ...@@ -13,10 +13,7 @@ class RootController < Dashboard::ProjectsController
before_action :redirect_logged_user, if: -> { current_user.present? } before_action :redirect_logged_user, if: -> { current_user.present? }
def index def index
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434 super
Gitlab::GitalyClient.allow_n_plus_1_calls do
super
end
end end
private private
......
...@@ -552,6 +552,7 @@ module Ci ...@@ -552,6 +552,7 @@ module Ci
variables = [ variables = [
{ key: 'CI', value: 'true', public: true }, { key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true }, { key: 'GITLAB_CI', value: 'true', public: true },
{ key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
......
...@@ -258,6 +258,10 @@ class Namespace < ActiveRecord::Base ...@@ -258,6 +258,10 @@ class Namespace < ActiveRecord::Base
all_projects.with_storage_feature(:repository).find_each(&:remove_exports) all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
end end
def features
[]
end
private private
def path_or_parent_changed? def path_or_parent_changed?
......
...@@ -5,7 +5,12 @@ ...@@ -5,7 +5,12 @@
%li.project-row{ class: ('no-description' if project.description.blank?) } %li.project-row{ class: ('no-description' if project.description.blank?) }
.controls .controls
= link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn" = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" %button.delete-project-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-project-modal',
delete_project_url: project_path(project),
project_name: project.name }, type: 'button' }
= s_('AdminProjects|Delete')
.stats .stats
%span.badge %span.badge
= storage_counter(project.statistics.storage_size) = storage_counter(project.statistics.storage_size)
...@@ -31,3 +36,5 @@ ...@@ -31,3 +36,5 @@
= paginate @projects, theme: 'gitlab' = paginate @projects, theme: 'gitlab'
- else - else
.nothing-here-block No projects found .nothing-here-block No projects found
#delete-project-modal
...@@ -44,12 +44,19 @@ ...@@ -44,12 +44,19 @@
%li.divider %li.divider
- if user.can_be_removed? - if user.can_be_removed?
%li %li
= link_to 'Remove user', admin_user_path(user), %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, target: '#delete-user-modal',
class: 'text-danger', delete_user_url: admin_user_path(user),
method: :delete block_user_url: block_admin_user_path(user),
username: user.name,
delete_contributions: 'false' }, type: 'button' }
= s_('AdminUsers|Delete user')
%li %li
= link_to 'Remove user and contributions', admin_user_path(user, hard_delete: true), %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and comments authored by this user, and groups owned solely by them, will also be removed! Are you sure?" }, target: '#delete-user-modal',
class: 'text-danger', delete_user_url: admin_user_path(user, hard_delete: true),
method: :delete block_user_url: block_admin_user_path(user),
username: user.name,
delete_contributions: 'true' }, type: 'button' }
= s_('AdminUsers|Delete user and contributions')
...@@ -77,3 +77,6 @@ ...@@ -77,3 +77,6 @@
= render partial: 'admin/users/user', collection: @users = render partial: 'admin/users/user', collection: @users
= paginate @users, theme: "gitlab" = paginate @users, theme: "gitlab"
#delete-user-modal
...@@ -176,13 +176,19 @@ ...@@ -176,13 +176,19 @@
.panel.panel-danger .panel.panel-danger
.panel-heading .panel-heading
Remove user = s_('AdminUsers|Delete user')
.panel-body .panel-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects: %p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user = render 'users/deletion_guidance', user: @user
%br %br
= link_to 'Remove user', admin_user_path(@user), data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user),
username: @user.name,
delete_contributions: 'false' }, type: 'button' }
= s_('AdminUsers|Delete user')
- else - else
- if @user.solo_owned_groups.present? - if @user.solo_owned_groups.present?
%p %p
...@@ -196,7 +202,7 @@ ...@@ -196,7 +202,7 @@
.panel.panel-danger .panel.panel-danger
.panel-heading .panel-heading
Remove user and contributions = s_('AdminUsers|Delete user and contributions')
.panel-body .panel-body
- if can?(current_user, :destroy_user, @user) - if can?(current_user, :destroy_user, @user)
%p %p
...@@ -208,7 +214,15 @@ ...@@ -208,7 +214,15 @@
the user, and projects in them, will also be removed. Commits the user, and projects in them, will also be removed. Commits
to other projects are unaffected. to other projects are unaffected.
%br %br
= link_to 'Remove user and contributions', admin_user_path(@user, hard_delete: true), data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user),
username: @user.name,
delete_contributions: 'true' }, type: 'button' }
= s_('AdminUsers|Delete user and contributions')
- else - else
%p %p
You don't have access to delete this user. You don't have access to delete this user.
#delete-user-modal
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.repo-charts{ class: container_class } .repo-charts{ class: container_class }
%h4.sub-header %h4.sub-header
Programming languages used in this repository = _("Programming languages used in this repository")
.row .row
.col-md-4 .col-md-4
...@@ -30,9 +30,11 @@ ...@@ -30,9 +30,11 @@
.row.tree-ref-header .row.tree-ref-header
.col-md-6 .col-md-6
%h4 %h4
Commit statistics for - start_time = capture do
%strong= @ref #{@commits_graph.start_date.strftime('%b %d')}
#{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} - end_time = capture do
#{@commits_graph.end_date.strftime('%b %d')}
= (_("Commit statistics for %{ref} %{start_time} - %{end_time}") % { ref: "<strong>#{@ref}</strong>", start_time: start_time, end_time: end_time }).html_safe
.col-md-6 .col-md-6
.tree-ref-container .tree-ref-container
...@@ -45,32 +47,35 @@ ...@@ -45,32 +47,35 @@
.col-md-6 .col-md-6
%ul.commit-stats %ul.commit-stats
%li %li
Total: - total = capture do
%strong #{@commits_graph.commits.size} commits #{@commits_graph.commits.size}
= (_("Total: %{total}") % { total: "<strong>#{total} commits</strong>" }).html_safe
%li %li
Average per day: - average = capture do
%strong #{@commits_graph.commit_per_day} commits #{@commits_graph.commit_per_day}
= (_("Average per day: %{average}") % { average: "<strong>#{average} commits</strong>" }).html_safe
%li %li
Authors: - authors = capture do
%strong= @commits_graph.authors #{@commits_graph.authors}
= (_("Authors: %{authors}") % { authors: "<strong>#{authors}</strong>" }).html_safe
.col-md-6 .col-md-6
%div %div
%p.slead %p.slead
Commits per day of month = _("Commits per day of month")
%canvas#month-chart %canvas#month-chart
.row .row
.col-md-6 .col-md-6
.col-md-6 .col-md-6
%div %div
%p.slead %p.slead
Commits per weekday = _("Commits per weekday")
%canvas#weekday-chart %canvas#weekday-chart
.row .row
.col-md-6 .col-md-6
.col-md-6 .col-md-6
%div %div
%p.slead %p.slead
Commits per day hour (UTC) = _("Commits per day hour (UTC)")
%canvas#hour-chart %canvas#hour-chart
%script#projectChartData{ type: "application/json" } %script#projectChartData{ type: "application/json" }
......
---
title: 'Expose GITLAB_FEATURES as CI/CD variable (fixes #40994)'
merge_request:
author:
type: added
---
title: Fix cnacel edit note button reverting changes
merge_request: 42462
author:
type: fixed
---
title: Avoid running `PopulateForkNetworksRange`-migration multiple times
merge_request: 16988
author:
type: fixed
---
title: Internationalize charts page
merge_request: 16687
author: selrahman
type: changed
...@@ -6,22 +6,8 @@ class PopulateForkNetworks < ActiveRecord::Migration ...@@ -6,22 +6,8 @@ class PopulateForkNetworks < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
MIGRATION = 'PopulateForkNetworksRange'.freeze
BATCH_SIZE = 100
DELAY_INTERVAL = 15.seconds
disable_ddl_transaction!
class ForkedProjectLink < ActiveRecord::Base
include EachBatch
self.table_name = 'forked_project_links'
end
def up def up
say 'Populating the `fork_networks` based on existing `forked_project_links`' say 'Fork networks will be populated in 20171205190711 - RescheduleForkNetworkCreationCaller'
queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
end end
def down def down
......
...@@ -3,22 +3,8 @@ class RescheduleForkNetworkCreation < ActiveRecord::Migration ...@@ -3,22 +3,8 @@ class RescheduleForkNetworkCreation < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
MIGRATION = 'PopulateForkNetworksRange'.freeze
BATCH_SIZE = 100
DELAY_INTERVAL = 15.seconds
disable_ddl_transaction!
class ForkedProjectLink < ActiveRecord::Base
include EachBatch
self.table_name = 'forked_project_links'
end
def up def up
say 'Populating the `fork_networks` based on existing `forked_project_links`' say 'Fork networks will be populated in 20171205190711 - RescheduleForkNetworkCreationCaller'
queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
end end
def down def down
......
...@@ -25,11 +25,11 @@ for each GitLab application server in your environment. ...@@ -25,11 +25,11 @@ for each GitLab application server in your environment.
options. Here is an example snippet to add to `/etc/fstab`: options. Here is an example snippet to add to `/etc/fstab`:
``` ```
10.1.0.1:/var/opt/gitlab/.ssh /var/opt/gitlab/.ssh nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 10.1.0.1:/var/opt/gitlab/.ssh /var/opt/gitlab/.ssh nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
10.1.0.1:/var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 10.1.0.1:/var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
10.1.0.1:/var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 10.1.0.1:/var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
10.1.0.1:/var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 10.1.0.1:/var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
10.1.1.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 10.1.0.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
``` ```
1. Create the shared directories. These may be different depending on your NFS 1. Create the shared directories. These may be different depending on your NFS
......
...@@ -94,6 +94,18 @@ jobs = [['BackgroundMigrationClassName', [1]], ...@@ -94,6 +94,18 @@ jobs = [['BackgroundMigrationClassName', [1]],
BackgroundMigrationWorker.bulk_perform_in(5.minutes, jobs) BackgroundMigrationWorker.bulk_perform_in(5.minutes, jobs)
``` ```
### Rescheduling background migrations
If one of the background migrations contains a bug that is fixed in a patch
release, the background migration needs to be rescheduled so the migration would
be repeated on systems that already performed the initial migration.
When you reschedule the background migration, make sure to turn the original
scheduling into a no-op by clearing up the `#up` and `#down` methods of the
migration performing the scheduling. Otherwise the background migration would be
scheduled multiple times on systems that are upgrading multiple patch releases at
once.
## Cleaning Up ## Cleaning Up
>**Note:** >**Note:**
......
...@@ -7,6 +7,8 @@ easy to maintain, and performant for the end-user. ...@@ -7,6 +7,8 @@ easy to maintain, and performant for the end-user.
### Naming ### Naming
Filenames should use `snake_case`.
CSS classes should use the `lowercase-hyphenated` format rather than CSS classes should use the `lowercase-hyphenated` format rather than
`snake_case` or `camelCase`. `snake_case` or `camelCase`.
......
--- ---
last_updated: 2017-10-05 last_updated: 2018-02-07
--- ---
# Migrating from MySQL to PostgreSQL # Migrating from MySQL to PostgreSQL
> **Note:** This guide assumes you have a working Omnibus GitLab instance with > **Note:** This guide assumes you have a working GitLab instance with
> MySQL and want to migrate to bundled PostgreSQL database. > MySQL and want to migrate to bundled PostgreSQL database.
## Prerequisites ## Omnibus installation
### Prerequisites
First, we'll need to enable the bundled PostgreSQL database with up-to-date First, we'll need to enable the bundled PostgreSQL database with up-to-date
schema. Next, we'll use [pgloader](http://pgloader.io) to migrate the data schema. Next, we'll use [pgloader](http://pgloader.io) to migrate the data
...@@ -19,7 +21,7 @@ Here's what you'll need to have installed: ...@@ -19,7 +21,7 @@ Here's what you'll need to have installed:
- Omnibus GitLab - Omnibus GitLab
- MySQL - MySQL
## Enable bundled PostgreSQL database ### Enable bundled PostgreSQL database
1. Stop GitLab: 1. Stop GitLab:
...@@ -65,7 +67,7 @@ Here's what you'll need to have installed: ...@@ -65,7 +67,7 @@ Here's what you'll need to have installed:
After these steps, you'll have a fresh PostgreSQL database with up-to-date schema. After these steps, you'll have a fresh PostgreSQL database with up-to-date schema.
## Migrate data from MySQL to PostgreSQL ### Migrate data from MySQL to PostgreSQL
Now, you can use pgloader to migrate the data from MySQL to PostgreSQL: Now, you can use pgloader to migrate the data from MySQL to PostgreSQL:
...@@ -104,122 +106,9 @@ the following: ...@@ -104,122 +106,9 @@ the following:
----------------------------------------------- --------- --------- --------- -------------- ----------------------------------------------- --------- --------- --------- --------------
public.abuse_reports 0 0 0 0.490s public.abuse_reports 0 0 0 0.490s
public.appearances 0 0 0 0.488s public.appearances 0 0 0 0.488s
public.approvals 0 0 0 0.273s .
public.application_settings 1 1 0 0.266s .
public.approvers 0 0 0 0.339s .
public.approver_groups 0 0 0 0.357s
public.audit_events 1 1 0 0.410s
public.award_emoji 0 0 0 0.441s
public.boards 0 0 0 0.505s
public.broadcast_messages 0 0 0 0.498s
public.chat_names 0 0 0 0.576s
public.chat_teams 0 0 0 0.617s
public.ci_builds 0 0 0 0.611s
public.ci_group_variables 0 0 0 0.620s
public.ci_pipelines 0 0 0 0.599s
public.ci_pipeline_schedules 0 0 0 0.622s
public.ci_pipeline_schedule_variables 0 0 0 0.573s
public.ci_pipeline_variables 0 0 0 0.594s
public.ci_runners 0 0 0 0.533s
public.ci_runner_projects 0 0 0 0.584s
public.ci_sources_pipelines 0 0 0 0.564s
public.ci_stages 0 0 0 0.595s
public.ci_triggers 0 0 0 0.569s
public.ci_trigger_requests 0 0 0 0.596s
public.ci_variables 0 0 0 0.565s
public.container_repositories 0 0 0 0.605s
public.conversational_development_index_metrics 0 0 0 0.571s
public.deployments 0 0 0 0.607s
public.emails 0 0 0 0.602s
public.deploy_keys_projects 0 0 0 0.557s
public.events 160 160 0 0.677s
public.environments 0 0 0 0.567s
public.features 0 0 0 0.639s
public.events_for_migration 160 160 0 0.582s
public.feature_gates 0 0 0 0.579s
public.forked_project_links 0 0 0 0.660s
public.geo_nodes 0 0 0 0.686s
public.geo_event_log 0 0 0 0.626s
public.geo_repositories_changed_events 0 0 0 0.677s
public.geo_node_namespace_links 0 0 0 0.618s
public.geo_repository_renamed_events 0 0 0 0.696s
public.gpg_keys 0 0 0 0.704s
public.geo_repository_deleted_events 0 0 0 0.638s
public.historical_data 0 0 0 0.729s
public.geo_repository_updated_events 0 0 0 0.634s
public.index_statuses 0 0 0 0.746s
public.gpg_signatures 0 0 0 0.667s
public.issue_assignees 80 80 0 0.769s
public.identities 0 0 0 0.655s
public.issue_metrics 80 80 0 0.781s
public.issues 80 80 0 0.720s
public.labels 0 0 0 0.795s
public.issue_links 0 0 0 0.707s
public.label_priorities 0 0 0 0.793s
public.keys 0 0 0 0.734s
public.lfs_objects 0 0 0 0.812s
public.label_links 0 0 0 0.725s
public.licenses 0 0 0 0.813s
public.ldap_group_links 0 0 0 0.751s
public.members 52 52 0 0.830s
public.lfs_objects_projects 0 0 0 0.738s
public.merge_requests_closing_issues 0 0 0 0.825s
public.lists 0 0 0 0.769s
public.merge_request_diff_commits 0 0 0 0.840s
public.merge_request_metrics 0 0 0 0.837s
public.merge_requests 0 0 0 0.753s
public.merge_request_diffs 0 0 0 0.771s
public.namespaces 30 30 0 0.874s
public.merge_request_diff_files 0 0 0 0.775s
public.notes 0 0 0 0.849s
public.milestones 40 40 0 0.799s
public.oauth_access_grants 0 0 0 0.979s
public.namespace_statistics 0 0 0 0.797s
public.oauth_applications 0 0 0 0.899s
public.notification_settings 72 72 0 0.818s
public.oauth_access_tokens 0 0 0 0.807s
public.pages_domains 0 0 0 0.958s
public.oauth_openid_requests 0 0 0 0.832s
public.personal_access_tokens 0 0 0 0.965s
public.projects 8 8 0 0.987s
public.path_locks 0 0 0 0.925s
public.plans 0 0 0 0.923s
public.project_features 8 8 0 0.985s
public.project_authorizations 66 66 0 0.969s
public.project_import_data 8 8 0 1.002s
public.project_statistics 8 8 0 1.001s
public.project_group_links 0 0 0 0.949s
public.project_mirror_data 0 0 0 0.972s
public.protected_branch_merge_access_levels 0 0 0 1.017s
public.protected_branches 0 0 0 0.969s
public.protected_branch_push_access_levels 0 0 0 0.991s
public.protected_tags 0 0 0 1.009s
public.protected_tag_create_access_levels 0 0 0 0.985s
public.push_event_payloads 0 0 0 1.041s
public.push_rules 0 0 0 0.999s
public.redirect_routes 0 0 0 1.020s
public.remote_mirrors 0 0 0 1.034s
public.releases 0 0 0 0.993s
public.schema_migrations 896 896 0 1.057s
public.routes 38 38 0 1.021s
public.services 0 0 0 1.055s
public.sent_notifications 0 0 0 1.003s
public.slack_integrations 0 0 0 1.022s
public.spam_logs 0 0 0 1.024s
public.snippets 0 0 0 1.058s
public.subscriptions 0 0 0 1.069s
public.taggings 0 0 0 1.099s
public.timelogs 0 0 0 1.104s
public.system_note_metadata 0 0 0 1.038s
public.tags 0 0 0 1.034s
public.trending_projects 0 0 0 1.140s
public.uploads 0 0 0 1.129s
public.todos 80 80 0 1.085s
public.users_star_projects 0 0 0 1.153s
public.u2f_registrations 0 0 0 1.061s
public.web_hooks 0 0 0 1.179s
public.users 26 26 0 1.163s
public.user_agent_details 0 0 0 1.068s
public.web_hook_logs 0 0 0 1.080s public.web_hook_logs 0 0 0 1.080s
----------------------------------------------- --------- --------- --------- -------------- ----------------------------------------------- --------- --------- --------- --------------
COPY Threads Completion 4 4 0 2.008s COPY Threads Completion 4 4 0 2.008s
...@@ -240,9 +129,9 @@ the following: ...@@ -240,9 +129,9 @@ the following:
Now, you can verify that everything worked by visiting GitLab. Now, you can verify that everything worked by visiting GitLab.
## Troubleshooting ### Troubleshooting
### Permissions #### Permissions
Note that the PostgreSQL user that you use for the above MUST have **superuser** privileges. Otherwise, you may see Note that the PostgreSQL user that you use for the above MUST have **superuser** privileges. Otherwise, you may see
a similar message to the following: a similar message to the following:
...@@ -256,7 +145,7 @@ debugger invoked on a CL-POSTGRES-ERROR:INSUFFICIENT-PRIVILEGE in thread ...@@ -256,7 +145,7 @@ debugger invoked on a CL-POSTGRES-ERROR:INSUFFICIENT-PRIVILEGE in thread
QUERY: ALTER TABLE approver_groups DISABLE TRIGGER ALL; QUERY: ALTER TABLE approver_groups DISABLE TRIGGER ALL;
``` ```
### Experiencing 500 errors after the migration #### Experiencing 500 errors after the migration
If you experience 500 errors after the migration, try to clear the cache: If you experience 500 errors after the migration, try to clear the cache:
...@@ -265,3 +154,130 @@ sudo gitlab-rake cache:clear ...@@ -265,3 +154,130 @@ sudo gitlab-rake cache:clear
``` ```
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
## Source installation
### Prerequisites
#### Install PostgreSQL and create database
See [installation guide](../install/installation.md#6-database).
#### Install [pgloader](http://pgloader.io) 3.4.1+
Install directly from your distro:
``` bash
sudo apt-get install pgloader
```
If this version is too old, use PostgreSQL's repository:
``` bash
# add repository
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
# add key
sudo apt-get install wget ca-certificates
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
# install package
sudo apt-get update
sudo apt-get install pgloader
```
### Enable bundled PostgreSQL database
1. Stop GitLab:
``` bash
sudo service gitlab stop
```
1. Switch database from MySQL to PostgreSQL
``` bash
cd /home/git/gitlab
sudo -u git mv config/database.yml config/database.yml.bak
sudo -u git cp config/database.yml.postgresql config/database.yml
sudo -u git -H chmod o-rwx config/database.yml
```
1. Run the following commands to prepare the schema:
``` bash
sudo -u git -H bundle exec rake db:create db:migrate RAILS_ENV=production
```
After these steps, you'll have a fresh PostgreSQL database with up-to-date schema.
### Migrate data from MySQL to PostgreSQL
Now, you can use pgloader to migrate the data from MySQL to PostgreSQL:
1. Save the following snippet in a `commands.load` file, and edit with your
MySQL `username`, `password` and `host`:
```
LOAD DATABASE
FROM mysql://username:password@host/gitlabhq_production
INTO postgresql://postgres@unix://var/run/postgresql:/gitlabhq_production
WITH include no drop, truncate, disable triggers, create no tables,
create no indexes, preserve index names, no foreign keys,
data only
ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public'
;
```
1. Start the migration:
``` bash
sudo -u postgres pgloader commands.load
```
1. Once the migration finishes, you should see a summary table that looks like
the following:
```
table name read imported errors total time
----------------------------------------------- --------- --------- --------- --------------
fetch meta data 119 119 0 0.388s
Truncate 119 119 0 1.134s
----------------------------------------------- --------- --------- --------- --------------
public.abuse_reports 0 0 0 0.490s
public.appearances 0 0 0 0.488s
.
.
.
public.web_hook_logs 0 0 0 1.080s
----------------------------------------------- --------- --------- --------- --------------
COPY Threads Completion 4 4 0 2.008s
Reset Sequences 113 113 0 0.304s
Install Comments 0 0 0 0.000s
----------------------------------------------- --------- --------- --------- --------------
Total import time 1894 1894 0 12.497s
```
If there is no output for more than 30 minutes, it's possible pgloader encountered an error. See
the [troubleshooting guide](#Troubleshooting) for more details.
1. Start GitLab:
``` bash
sudo service gitlab start
```
Now, you can verify that everything worked by visiting GitLab.
### Troubleshooting
#### Experiencing 500 errors after the migration
If you experience 500 errors after the migration, try to clear the cache:
``` bash
sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
```
# Deleting a User Account # Deleting a User Account
- As a user, you can delete your own account by navigating to **Settings** > **Account** and selecting **Delete account** - As a user, you can delete your own account by navigating to **Settings** > **Account** and selecting **Delete account**
- As an admin, you can delete a user account by navigating to the **Admin Area**, selecting the **Users** tab, selecting a user, and clicking on **Remove user** - As an admin, you can delete a user account by navigating to the **Admin Area**, selecting the **Users** tab, selecting a user, and clicking on **Delete user**
## Associated Records ## Associated Records
......
...@@ -14,6 +14,14 @@ module Gitlab ...@@ -14,6 +14,14 @@ module Gitlab
def perform(start_id, end_id) def perform(start_id, end_id)
log("Creating memberships for forks: #{start_id} - #{end_id}") log("Creating memberships for forks: #{start_id} - #{end_id}")
insert_members(start_id, end_id)
if missing_members?(start_id, end_id)
BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [start_id, end_id])
end
end
def insert_members(start_id, end_id)
ActiveRecord::Base.connection.execute <<~INSERT_MEMBERS ActiveRecord::Base.connection.execute <<~INSERT_MEMBERS
INSERT INTO fork_network_members (fork_network_id, project_id, forked_from_project_id) INSERT INTO fork_network_members (fork_network_id, project_id, forked_from_project_id)
...@@ -33,10 +41,9 @@ module Gitlab ...@@ -33,10 +41,9 @@ module Gitlab
WHERE existing_members.project_id = forked_project_links.forked_to_project_id WHERE existing_members.project_id = forked_project_links.forked_to_project_id
) )
INSERT_MEMBERS INSERT_MEMBERS
rescue ActiveRecord::RecordNotUnique => e
if missing_members?(start_id, end_id) # `fork_network_member` was created concurrently in another migration
BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [start_id, end_id]) log(e.message)
end
end end
def missing_members?(start_id, end_id) def missing_members?(start_id, end_id)
......
...@@ -53,11 +53,7 @@ module Gitlab ...@@ -53,11 +53,7 @@ module Gitlab
def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE) def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE)
Gitlab::GitalyClient.migrate(:list_blobs_by_sha_path) do |is_enabled| Gitlab::GitalyClient.migrate(:list_blobs_by_sha_path) do |is_enabled|
if is_enabled if is_enabled
Gitlab::GitalyClient.allow_n_plus_1_calls do repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a
blob_references.map do |sha, path|
find_by_gitaly(repository, sha, path, limit: blob_size_limit)
end
end
else else
blob_references.map do |sha, path| blob_references.map do |sha, path|
find_by_rugged(repository, sha, path, limit: blob_size_limit) find_by_rugged(repository, sha, path, limit: blob_size_limit)
......
module Gitlab module Gitlab
module GitalyClient module GitalyClient
class BlobService class BlobService
include Gitlab::EncodingHelper
def initialize(repository) def initialize(repository)
@gitaly_repo = repository.gitaly_repository @gitaly_repo = repository.gitaly_repository
end end
...@@ -54,6 +56,30 @@ module Gitlab ...@@ -54,6 +56,30 @@ module Gitlab
end end
end end
end end
def get_blobs(revision_paths, limit = -1)
return [] if revision_paths.empty?
revision_paths.map! do |rev, path|
Gitaly::GetBlobsRequest::RevisionPath.new(revision: rev, path: encode_binary(path))
end
request = Gitaly::GetBlobsRequest.new(
repository: @gitaly_repo,
revision_paths: revision_paths,
limit: limit
)
response = GitalyClient.call(
@gitaly_repo.storage_name,
:blob_service,
:get_blobs,
request,
timeout: GitalyClient.default_timeout
)
GitalyClient::BlobsStitcher.new(response)
end
end end
end end
end end
module Gitlab
module GitalyClient
class BlobsStitcher
include Enumerable
def initialize(rpc_response)
@rpc_response = rpc_response
end
def each
current_blob_data = nil
@rpc_response.each do |msg|
begin
if msg.oid.blank? && msg.data.blank?
next
elsif msg.oid.present?
yield new_blob(current_blob_data) if current_blob_data
current_blob_data = msg.to_h.slice(:oid, :path, :size, :revision, :mode)
current_blob_data[:data] = msg.data.dup
else
current_blob_data[:data] << msg.data
end
end
end
yield new_blob(current_blob_data) if current_blob_data
end
private
def new_blob(blob_data)
Gitlab::Git::Blob.new(
id: blob_data[:oid],
mode: blob_data[:mode].to_s(8),
name: File.basename(blob_data[:path]),
path: blob_data[:path],
size: blob_data[:size],
commit_id: blob_data[:revision],
data: blob_data[:data],
binary: Gitlab::Git::Blob.binary?(blob_data[:data])
)
end
end
end
end
...@@ -294,7 +294,8 @@ module Gitlab ...@@ -294,7 +294,8 @@ module Gitlab
# add_namespace("/path/to/storage", "gitlab") # add_namespace("/path/to/storage", "gitlab")
# #
def add_namespace(storage, name) def add_namespace(storage, name)
Gitlab::GitalyClient.migrate(:add_namespace) do |enabled| Gitlab::GitalyClient.migrate(:add_namespace,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled if enabled
gitaly_namespace_client(storage).add(name) gitaly_namespace_client(storage).add(name)
else else
...@@ -315,7 +316,8 @@ module Gitlab ...@@ -315,7 +316,8 @@ module Gitlab
# rm_namespace("/path/to/storage", "gitlab") # rm_namespace("/path/to/storage", "gitlab")
# #
def rm_namespace(storage, name) def rm_namespace(storage, name)
Gitlab::GitalyClient.migrate(:remove_namespace) do |enabled| Gitlab::GitalyClient.migrate(:remove_namespace,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled if enabled
gitaly_namespace_client(storage).remove(name) gitaly_namespace_client(storage).remove(name)
else else
...@@ -333,7 +335,8 @@ module Gitlab ...@@ -333,7 +335,8 @@ module Gitlab
# mv_namespace("/path/to/storage", "gitlab", "gitlabhq") # mv_namespace("/path/to/storage", "gitlab", "gitlabhq")
# #
def mv_namespace(storage, old_name, new_name) def mv_namespace(storage, old_name, new_name)
Gitlab::GitalyClient.migrate(:rename_namespace) do |enabled| Gitlab::GitalyClient.migrate(:rename_namespace,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled if enabled
gitaly_namespace_client(storage).rename(old_name, new_name) gitaly_namespace_client(storage).rename(old_name, new_name)
else else
...@@ -368,7 +371,8 @@ module Gitlab ...@@ -368,7 +371,8 @@ module Gitlab
# #
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385 # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def exists?(storage, dir_name) def exists?(storage, dir_name)
Gitlab::GitalyClient.migrate(:namespace_exists) do |enabled| Gitlab::GitalyClient.migrate(:namespace_exists,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled if enabled
gitaly_namespace_client(storage).exists?(dir_name) gitaly_namespace_client(storage).exists?(dir_name)
else else
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-07 13:35+0100\n" "POT-Creation-Date: 2018-02-07 11:38-0600\n"
"PO-Revision-Date: 2018-02-07 13:35+0100\n" "PO-Revision-Date: 2018-02-07 11:38-0600\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -279,6 +279,9 @@ msgstr "" ...@@ -279,6 +279,9 @@ msgstr ""
msgid "Author" msgid "Author"
msgstr "" msgstr ""
msgid "Authors: %{authors}"
msgstr ""
msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly." msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
msgstr "" msgstr ""
...@@ -312,6 +315,7 @@ msgstr "" ...@@ -312,6 +315,7 @@ msgstr ""
msgid "Avatar will be removed. Are you sure?" msgid "Avatar will be removed. Are you sure?"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Billing" msgid "Billing"
msgstr "" msgstr ""
...@@ -364,6 +368,9 @@ msgid "BillingPlans|paid annually at %{price_per_year}" ...@@ -364,6 +368,9 @@ msgid "BillingPlans|paid annually at %{price_per_year}"
msgstr "" msgstr ""
msgid "BillingPlans|per user" msgid "BillingPlans|per user"
=======
msgid "Average per day: %{average}"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "Begin with the selected commit" msgid "Begin with the selected commit"
...@@ -830,6 +837,9 @@ msgstr "" ...@@ -830,6 +837,9 @@ msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters" msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
msgstr "" msgstr ""
msgid "ClusterIntegration|Manage"
msgstr ""
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}" msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
msgstr "" msgstr ""
...@@ -970,6 +980,9 @@ msgstr "" ...@@ -970,6 +980,9 @@ msgstr ""
msgid "Commit message" msgid "Commit message"
msgstr "" msgstr ""
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
msgstr ""
msgid "CommitBoxTitle|Commit" msgid "CommitBoxTitle|Commit"
msgstr "" msgstr ""
...@@ -982,6 +995,15 @@ msgstr "" ...@@ -982,6 +995,15 @@ msgstr ""
msgid "Commits feed" msgid "Commits feed"
msgstr "" msgstr ""
msgid "Commits per day hour (UTC)"
msgstr ""
msgid "Commits per day of month"
msgstr ""
msgid "Commits per weekday"
msgstr ""
msgid "Commits|An error occurred while fetching merge requests data." msgid "Commits|An error occurred while fetching merge requests data."
msgstr "" msgstr ""
...@@ -1490,6 +1512,7 @@ msgstr "" ...@@ -1490,6 +1512,7 @@ msgstr ""
msgid "Generate a default set of labels" msgid "Generate a default set of labels"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Geo Nodes" msgid "Geo Nodes"
msgstr "" msgstr ""
...@@ -1604,6 +1627,8 @@ msgstr "" ...@@ -1604,6 +1627,8 @@ msgstr ""
msgid "Geo|Shards to synchronize" msgid "Geo|Shards to synchronize"
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "Git revision" msgid "Git revision"
msgstr "" msgstr ""
...@@ -2368,6 +2393,9 @@ msgstr "" ...@@ -2368,6 +2393,9 @@ msgstr ""
msgid "Profiles|your account" msgid "Profiles|your account"
msgstr "" msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
msgid "Project '%{project_name}' is in the process of being deleted." msgid "Project '%{project_name}' is in the process of being deleted."
msgstr "" msgstr ""
...@@ -3338,10 +3366,14 @@ msgstr "" ...@@ -3338,10 +3366,14 @@ msgstr ""
msgid "Total test time for all commits/merges" msgid "Total test time for all commits/merges"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Track activity with Contribution Analytics." msgid "Track activity with Contribution Analytics."
msgstr "" msgstr ""
msgid "Track groups of issues that share a theme, across projects and milestones" msgid "Track groups of issues that share a theme, across projects and milestones"
=======
msgid "Total: %{total}"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "Track time with quick actions" msgid "Track time with quick actions"
...@@ -3590,6 +3622,9 @@ msgstr "" ...@@ -3590,6 +3622,9 @@ msgstr ""
msgid "You can move around the graph by using the arrow keys." msgid "You can move around the graph by using the arrow keys."
msgstr "" msgstr ""
msgid "You can move around the graph by using the arrow keys."
msgstr ""
msgid "You can only add files when you are on a branch" msgid "You can only add files when you are on a branch"
msgstr "" msgstr ""
......
...@@ -26,8 +26,8 @@ describe "Admin::Users" do ...@@ -26,8 +26,8 @@ describe "Admin::Users" do
expect(page).to have_content(user.email) expect(page).to have_content(user.email)
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect(page).to have_link('Block', href: block_admin_user_path(user)) expect(page).to have_link('Block', href: block_admin_user_path(user))
expect(page).to have_link('Remove user', href: admin_user_path(user)) expect(page).to have_button('Delete user')
expect(page).to have_link('Remove user and contributions', href: admin_user_path(user, hard_delete: true)) expect(page).to have_button('Delete user and contributions')
end end
describe 'Two-factor Authentication filters' do describe 'Two-factor Authentication filters' do
...@@ -122,8 +122,8 @@ describe "Admin::Users" do ...@@ -122,8 +122,8 @@ describe "Admin::Users" do
expect(page).to have_content(user.email) expect(page).to have_content(user.email)
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect(page).to have_link('Block user', href: block_admin_user_path(user)) expect(page).to have_link('Block user', href: block_admin_user_path(user))
expect(page).to have_link('Remove user', href: admin_user_path(user)) expect(page).to have_button('Delete user')
expect(page).to have_link('Remove user and contributions', href: admin_user_path(user, hard_delete: true)) expect(page).to have_button('Delete user and contributions')
end end
describe 'Impersonation' do describe 'Impersonation' do
......
...@@ -12,4 +12,13 @@ describe 'User visits their profile' do ...@@ -12,4 +12,13 @@ describe 'User visits their profile' do
it 'shows correct menu item' do it 'shows correct menu item' do
expect(page).to have_active_navigation('Profile') expect(page).to have_active_navigation('Profile')
end end
describe 'profile settings', :js do
it 'saves updates' do
fill_in 'user_bio', with: 'bio'
click_button 'Update profile settings'
expect(page).to have_content('Profile was successfully updated')
end
end
end end
...@@ -109,7 +109,8 @@ describe 'Pipelines', :js do ...@@ -109,7 +109,8 @@ describe 'Pipelines', :js do
context 'when canceling' do context 'when canceling' do
before do before do
accept_confirm { find('.js-pipelines-cancel-button').click } find('.js-pipelines-cancel-button').click
find('.js-primary-button').click
wait_for_requests wait_for_requests
end end
...@@ -140,6 +141,7 @@ describe 'Pipelines', :js do ...@@ -140,6 +141,7 @@ describe 'Pipelines', :js do
context 'when retrying' do context 'when retrying' do
before do before do
find('.js-pipelines-retry-button').click find('.js-pipelines-retry-button').click
find('.js-primary-button').click
wait_for_requests wait_for_requests
end end
...@@ -238,7 +240,8 @@ describe 'Pipelines', :js do ...@@ -238,7 +240,8 @@ describe 'Pipelines', :js do
context 'when canceling' do context 'when canceling' do
before do before do
accept_alert { find('.js-pipelines-cancel-button').click } find('.js-pipelines-cancel-button').click
find('.js-primary-button').click
end end
it 'indicates that pipeline was canceled' do it 'indicates that pipeline was canceled' do
......
...@@ -268,10 +268,10 @@ describe('AppComponent', () => { ...@@ -268,10 +268,10 @@ describe('AppComponent', () => {
it('updates props which show modal confirmation dialog', () => { it('updates props which show modal confirmation dialog', () => {
const group = Object.assign({}, mockParentGroupItem); const group = Object.assign({}, mockParentGroupItem);
expect(vm.showModal).toBeFalsy(); expect(vm.updateModal).toBeFalsy();
expect(vm.groupLeaveConfirmationMessage).toBe(''); expect(vm.groupLeaveConfirmationMessage).toBe('');
vm.showLeaveGroupModal(group, mockParentGroupItem); vm.showLeaveGroupModal(group, mockParentGroupItem);
expect(vm.showModal).toBeTruthy(); expect(vm.updateModal).toBeTruthy();
expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`); expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`);
}); });
}); });
...@@ -280,9 +280,9 @@ describe('AppComponent', () => { ...@@ -280,9 +280,9 @@ describe('AppComponent', () => {
it('hides modal confirmation which is shown before leaving the group', () => { it('hides modal confirmation which is shown before leaving the group', () => {
const group = Object.assign({}, mockParentGroupItem); const group = Object.assign({}, mockParentGroupItem);
vm.showLeaveGroupModal(group, mockParentGroupItem); vm.showLeaveGroupModal(group, mockParentGroupItem);
expect(vm.showModal).toBeTruthy(); expect(vm.updateModal).toBeTruthy();
vm.hideLeaveGroupModal(); vm.hideLeaveGroupModal();
expect(vm.showModal).toBeFalsy(); expect(vm.updateModal).toBeFalsy();
}); });
}); });
...@@ -307,7 +307,7 @@ describe('AppComponent', () => { ...@@ -307,7 +307,7 @@ describe('AppComponent', () => {
spyOn($, 'scrollTo'); spyOn($, 'scrollTo');
vm.leaveGroup(); vm.leaveGroup();
expect(vm.showModal).toBeFalsy(); expect(vm.updateModal).toBeFalsy();
expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); expect(vm.targetGroup.isBeingRemoved).toBeTruthy();
expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath); expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath);
setTimeout(() => { setTimeout(() => {
...@@ -475,7 +475,7 @@ describe('AppComponent', () => { ...@@ -475,7 +475,7 @@ describe('AppComponent', () => {
it('renders modal confirmation dialog', () => { it('renders modal confirmation dialog', () => {
vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?';
vm.showModal = true; vm.updateModal = true;
const modalDialogEl = vm.$el.querySelector('.modal'); const modalDialogEl = vm.$el.querySelector('.modal');
expect(modalDialogEl).not.toBe(null); expect(modalDialogEl).not.toBe(null);
expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?');
......
...@@ -2,14 +2,29 @@ import _ from 'underscore'; ...@@ -2,14 +2,29 @@ import _ from 'underscore';
import Vue from 'vue'; import Vue from 'vue';
import notesApp from '~/notes/components/notes_app.vue'; import notesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service'; import service from '~/notes/services/notes_service';
import '~/render_gfm';
import * as mockData from '../mock_data'; import * as mockData from '../mock_data';
import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
const vueMatchers = {
toIncludeElement() {
return {
compare(vm, selector) {
const result = {
pass: vm.$el.querySelector(selector) !== null,
};
return result;
},
};
},
};
describe('note_app', () => { describe('note_app', () => {
let mountComponent; let mountComponent;
let vm; let vm;
beforeEach(() => { beforeEach(() => {
jasmine.addMatchers(vueMatchers);
const IssueNotesApp = Vue.extend(notesApp); const IssueNotesApp = Vue.extend(notesApp);
mountComponent = (data) => { mountComponent = (data) => {
...@@ -105,7 +120,7 @@ describe('note_app', () => { ...@@ -105,7 +120,7 @@ describe('note_app', () => {
}); });
it('should render loading icon', () => { it('should render loading icon', () => {
expect(vm.$el.querySelector('.js-loading')).toBeDefined(); expect(vm).toIncludeElement('.js-loading');
}); });
it('should render form', () => { it('should render form', () => {
...@@ -118,10 +133,14 @@ describe('note_app', () => { ...@@ -118,10 +133,14 @@ describe('note_app', () => {
describe('update note', () => { describe('update note', () => {
describe('individual note', () => { describe('individual note', () => {
beforeEach(() => { beforeEach((done) => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor); Vue.http.interceptors.push(mockData.individualNoteInterceptor);
spyOn(service, 'updateNote').and.callThrough(); spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent(); vm = mountComponent();
setTimeout(() => {
vm.$el.querySelector('.js-note-edit').click();
Vue.nextTick(done);
}, 0);
}); });
afterEach(() => { afterEach(() => {
...@@ -131,40 +150,32 @@ describe('note_app', () => { ...@@ -131,40 +150,32 @@ describe('note_app', () => {
); );
}); });
it('renders edit form', (done) => { it('renders edit form', () => {
setTimeout(() => { expect(vm).toIncludeElement('.js-vue-issue-note-form');
vm.$el.querySelector('.js-note-edit').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined();
done();
});
}, 0);
}); });
it('calls the service to update the note', (done) => { it('calls the service to update the note', (done) => {
getSetTimeoutPromise() vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
.then(() => { vm.$el.querySelector('.js-vue-issue-save').click();
vm.$el.querySelector('.js-note-edit').click();
}) expect(service.updateNote).toHaveBeenCalled();
.then(Vue.nextTick) // Wait for the requests to finish before destroying
.then(() => { Vue.nextTick()
vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
vm.$el.querySelector('.js-vue-issue-save').click();
expect(service.updateNote).toHaveBeenCalled();
})
// Wait for the requests to finish before destroying
.then(Vue.nextTick)
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('dicussion note', () => { describe('discussion note', () => {
beforeEach(() => { beforeEach((done) => {
Vue.http.interceptors.push(mockData.discussionNoteInterceptor); Vue.http.interceptors.push(mockData.discussionNoteInterceptor);
spyOn(service, 'updateNote').and.callThrough(); spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent(); vm = mountComponent();
setTimeout(() => {
vm.$el.querySelector('.js-note-edit').click();
Vue.nextTick(done);
}, 0);
}); });
afterEach(() => { afterEach(() => {
...@@ -174,30 +185,17 @@ describe('note_app', () => { ...@@ -174,30 +185,17 @@ describe('note_app', () => {
); );
}); });
it('renders edit form', (done) => { it('renders edit form', () => {
setTimeout(() => { expect(vm).toIncludeElement('.js-vue-issue-note-form');
vm.$el.querySelector('.js-note-edit').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined();
done();
});
}, 0);
}); });
it('updates the note and resets the edit form', (done) => { it('updates the note and resets the edit form', (done) => {
getSetTimeoutPromise() vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
.then(() => { vm.$el.querySelector('.js-vue-issue-save').click();
vm.$el.querySelector('.js-note-edit').click();
}) expect(service.updateNote).toHaveBeenCalled();
.then(Vue.nextTick) // Wait for the requests to finish before destroying
.then(() => { Vue.nextTick()
vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
vm.$el.querySelector('.js-vue-issue-save').click();
expect(service.updateNote).toHaveBeenCalled();
})
// Wait for the requests to finish before destroying
.then(Vue.nextTick)
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
......
...@@ -56,4 +56,25 @@ describe('issue_note', () => { ...@@ -56,4 +56,25 @@ describe('issue_note', () => {
done(); done();
}, 0); }, 0);
}); });
describe('cancel edit', () => {
it('restores content of updated note', (done) => {
const noteBody = 'updated note text';
vm.updateNote = () => Promise.resolve();
vm.formUpdateHandler(noteBody, null, $.noop);
setTimeout(() => {
expect(vm.note.note_html).toEqual(noteBody);
vm.formCancelHandler();
setTimeout(() => {
expect(vm.note.note_html).toEqual(noteBody);
done();
});
});
});
});
}); });
...@@ -15,6 +15,7 @@ describe('Pipelines Async Button', () => { ...@@ -15,6 +15,7 @@ describe('Pipelines Async Button', () => {
title: 'Foo', title: 'Foo',
icon: 'repeat', icon: 'repeat',
cssClass: 'bar', cssClass: 'bar',
id: 123,
}, },
}).$mount(); }).$mount();
}); });
...@@ -38,9 +39,8 @@ describe('Pipelines Async Button', () => { ...@@ -38,9 +39,8 @@ describe('Pipelines Async Button', () => {
describe('With confirm dialog', () => { describe('With confirm dialog', () => {
it('should call the service when confimation is positive', () => { it('should call the service when confimation is positive', () => {
spyOn(window, 'confirm').and.returnValue(true); eventHub.$on('actionConfirmationModal', (data) => {
eventHub.$on('postAction', (endpoint) => { expect(data.id).toEqual(123);
expect(endpoint).toEqual('/foo');
}); });
component = new AsyncButtonComponent({ component = new AsyncButtonComponent({
...@@ -49,7 +49,7 @@ describe('Pipelines Async Button', () => { ...@@ -49,7 +49,7 @@ describe('Pipelines Async Button', () => {
title: 'Foo', title: 'Foo',
icon: 'fa fa-foo', icon: 'fa fa-foo',
cssClass: 'bar', cssClass: 'bar',
confirmActionMessage: 'bar', id: 123,
}, },
}).$mount(); }).$mount();
......
...@@ -178,67 +178,77 @@ describe Gitlab::Git::Blob, seed_helper: true do ...@@ -178,67 +178,77 @@ describe Gitlab::Git::Blob, seed_helper: true do
end end
describe '.batch' do describe '.batch' do
let(:blob_references) do shared_examples 'loading blobs in batch' do
[ let(:blob_references) do
[SeedRepo::Commit::ID, "files/ruby/popen.rb"], [
[SeedRepo::Commit::ID, 'six'] [SeedRepo::Commit::ID, "files/ruby/popen.rb"],
] [SeedRepo::Commit::ID, 'six']
end ]
end
subject { described_class.batch(repository, blob_references) } subject { described_class.batch(repository, blob_references) }
it { expect(subject.size).to eq(blob_references.size) } it { expect(subject.size).to eq(blob_references.size) }
context 'first blob' do context 'first blob' do
let(:blob) { subject[0] } let(:blob) { subject[0] }
it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) } it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) } it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
it { expect(blob.path).to eq("files/ruby/popen.rb") } it { expect(blob.path).to eq("files/ruby/popen.rb") }
it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) } it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) }
it { expect(blob.size).to eq(669) } it { expect(blob.size).to eq(669) }
it { expect(blob.mode).to eq("100644") } it { expect(blob.mode).to eq("100644") }
end end
context 'second blob' do context 'second blob' do
let(:blob) { subject[1] } let(:blob) { subject[1] }
it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') } it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
it { expect(blob.data).to eq('') } it { expect(blob.data).to eq('') }
it 'does not mark the blob as binary' do it 'does not mark the blob as binary' do
expect(blob).not_to be_binary expect(blob).not_to be_binary
end
end end
end
context 'limiting' do context 'limiting' do
subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) } subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) }
context 'positive' do context 'positive' do
let(:blob_size_limit) { 10 } let(:blob_size_limit) { 10 }
it { expect(subject.first.data.size).to eq(10) } it { expect(subject.first.data.size).to eq(10) }
end end
context 'zero' do context 'zero' do
let(:blob_size_limit) { 0 } let(:blob_size_limit) { 0 }
it 'only loads the metadata' do it 'only loads the metadata' do
expect(subject.first.size).not_to be(0) expect(subject.first.size).not_to be(0)
expect(subject.first.data).to eq('') expect(subject.first.data).to eq('')
end
end end
end
context 'negative' do context 'negative' do
let(:blob_size_limit) { -1 } let(:blob_size_limit) { -1 }
it 'ignores MAX_DATA_DISPLAY_SIZE' do it 'ignores MAX_DATA_DISPLAY_SIZE' do
stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100) stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100)
expect(subject.first.data.size).to eq(669) expect(subject.first.data.size).to eq(669)
end
end end
end end
end end
context 'when Gitaly list_blobs_by_sha_path feature is enabled' do
it_behaves_like 'loading blobs in batch'
end
context 'when Gitaly list_blobs_by_sha_path feature is disabled', :disable_gitaly do
it_behaves_like 'loading blobs in batch'
end
end end
describe '.batch_lfs_pointers' do describe '.batch_lfs_pointers' do
......
require 'spec_helper'
describe Gitlab::GitalyClient::BlobsStitcher do
describe 'enumeration' do
it 'combines segregated blob messages together' do
messages = [
OpenStruct.new(oid: 'abcdef1', path: 'path/to/file', size: 1642, revision: 'f00ba7', mode: 0100644, data: "first-line\n"),
OpenStruct.new(oid: '', data: 'second-line'),
OpenStruct.new(oid: '', data: '', revision: 'f00ba7', path: 'path/to/non-existent/file'),
OpenStruct.new(oid: 'abcdef2', path: 'path/to/another-file', size: 2461, revision: 'f00ba8', mode: 0100644, data: "GIF87a\x90\x01".b)
]
blobs = described_class.new(messages).to_a
expect(blobs.size).to be(2)
expect(blobs[0].id).to eq('abcdef1')
expect(blobs[0].mode).to eq('100644')
expect(blobs[0].name).to eq('file')
expect(blobs[0].path).to eq('path/to/file')
expect(blobs[0].size).to eq(1642)
expect(blobs[0].commit_id).to eq('f00ba7')
expect(blobs[0].data).to eq("first-line\nsecond-line")
expect(blobs[0].binary?).to be false
expect(blobs[1].id).to eq('abcdef2')
expect(blobs[1].mode).to eq('100644')
expect(blobs[1].name).to eq('another-file')
expect(blobs[1].path).to eq('path/to/another-file')
expect(blobs[1].size).to eq(2461)
expect(blobs[1].commit_id).to eq('f00ba8')
expect(blobs[1].data).to eq("GIF87a\x90\x01".b)
expect(blobs[1].binary?).to be true
end
end
end
...@@ -1432,6 +1432,7 @@ describe Ci::Build do ...@@ -1432,6 +1432,7 @@ describe Ci::Build do
[ [
{ key: 'CI', value: 'true', public: true }, { key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true }, { key: 'GITLAB_CI', value: 'true', public: true },
{ key: 'GITLAB_FEATURES', value: '', public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
......
...@@ -103,6 +103,7 @@ performance: ...@@ -103,6 +103,7 @@ performance:
artifacts: artifacts:
paths: paths:
- performance.json - performance.json
- sitespeed-results/
only: only:
refs: refs:
- branches - branches
...@@ -503,16 +504,16 @@ production: ...@@ -503,16 +504,16 @@ production:
export CI_ENVIRONMENT_URL=$(cat environment_url.txt) export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
mkdir gitlab-exporter mkdir gitlab-exporter
wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-3/index.js wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-5/index.js
mkdir sitespeed-results mkdir sitespeed-results
if [ -f .gitlab-urls.txt ] if [ -f .gitlab-urls.txt ]
then then
sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt
docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.0.3 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt
else else
docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.0.3 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
fi fi
mv sitespeed-results/data/performance.json performance.json mv sitespeed-results/data/performance.json performance.json
......
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