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
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.7.0.beta44'
gem 'prometheus-client-mmap', '~> 0.9.1'
gem 'raindrops', '~> 0.18'
end
......@@ -426,7 +426,7 @@ group :ed25519 do
end
# 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
gem 'google-protobuf', '= 3.5.1'
......
......@@ -309,7 +309,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.83.0)
gitaly-proto (0.84.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -665,7 +665,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta44)
prometheus-client-mmap (0.9.1)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
......@@ -1091,7 +1091,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.83.0)
gitaly-proto (~> 0.84.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......@@ -1170,7 +1170,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta44)
prometheus-client-mmap (~> 0.9.1)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
......
......@@ -133,6 +133,21 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
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:starred':
import('./pages/dashboard/projects')
......
......@@ -152,14 +152,14 @@ export default {
showLeaveGroupModal(group, parentGroup) {
this.targetGroup = group;
this.targetParentGroup = parentGroup;
this.showModal = true;
this.updateModal = true;
this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`);
},
hideLeaveGroupModal() {
this.showModal = false;
this.updateModal = false;
},
leaveGroup() {
this.showModal = false;
this.updateModal = false;
this.targetGroup.isBeingRemoved = true;
this.service.leaveGroup(this.targetGroup.leavePath)
.then(res => res.json())
......
......@@ -2,7 +2,9 @@
/* global ace */
import Vue from 'vue';
import Flash from '../../flash';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
......@@ -49,27 +51,26 @@ import Flash from '../../flash';
loadEditor() {
this.loading = true;
$.get(this.file.content_path)
.done((file) => {
axios.get(this.file.content_path)
.then(({ data }) => {
const content = this.$el.querySelector('pre');
const fileContent = document.createTextNode(file.content);
const fileContent = document.createTextNode(data.content);
content.textContent = fileContent.textContent;
this.originalContent = file.content;
this.originalContent = data.content;
this.fileLoaded = true;
this.editor = ace.edit(content);
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.saveDiffResolution();
});
this.saveDiffResolution();
this.loading = false;
})
.fail(() => {
new Flash('Failed to load the file, please try again.');
})
.always(() => {
.catch(() => {
flash(__('An error occurred while loading the file'));
this.loading = false;
});
},
......
......@@ -102,6 +102,7 @@
.then(() => {
this.isEditing = false;
this.isRequesting = false;
this.oldContent = null;
$(this.$refs.noteBody.$el).renderGFM();
this.$refs.noteBody.resetAutoSave();
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 @@
type: String,
required: true,
},
<<<<<<< HEAD
confirmActionMessage: {
type: String,
required: false,
=======
id: {
type: Number,
required: true,
>>>>>>> upstream/master
},
},
......@@ -50,11 +56,18 @@
},
methods: {
onClick() {
<<<<<<< HEAD
if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
this.makeRequest();
} else if (!this.confirmActionMessage) {
this.makeRequest();
}
=======
eventHub.$emit('actionConfirmationModal', {
id: this.id,
callback: this.makeRequest,
});
>>>>>>> upstream/master
},
makeRequest() {
this.isLoading = true;
......
<script>
import pipelinesTableRowComponent from './pipelines_table_row.vue';
import stopConfirmationModal from './stop_confirmation_modal.vue';
import retryConfirmationModal from './retry_confirmation_modal.vue';
/**
* Pipelines Table Component.
......@@ -9,6 +11,8 @@
export default {
components: {
pipelinesTableRowComponent,
stopConfirmationModal,
retryConfirmationModal,
},
props: {
pipelines: {
......@@ -70,5 +74,7 @@
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/>
<stop-confirmation-modal />
<retry-confirmation-modal />
</div>
</template>
......@@ -306,6 +306,9 @@
css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry"
icon="repeat"
:id="pipeline.id"
data-toggle="modal"
data-target="#retry-confirmation-modal"
/>
<async-button-component
......@@ -314,7 +317,9 @@
css-class="js-pipelines-cancel-button btn-remove"
title="Cancel"
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>
......
<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 */
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) => {
class Profile {
......@@ -57,8 +59,8 @@ import { getPagePath } from '../lib/utils/common_utils';
onUpdateNotifs(e, data) {
return data.saved ?
new Flash("Notification settings saved", "notice") :
new Flash("Failed to save new settings", "alert");
flash(__('Notification settings saved'), 'notice') :
flash(__('Failed to save new settings'));
}
saveForm() {
......@@ -70,21 +72,18 @@ import { getPagePath } from '../lib/utils/common_utils';
formData.append('user[avatar]', avatarBlob, 'avatar.png');
}
return $.ajax({
axios({
method: this.form.attr('method'),
url: this.form.attr('action'),
type: this.form.attr('method'),
data: formData,
dataType: "json",
processData: false,
contentType: false,
success: response => new Flash(response.message, 'notice'),
error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'),
complete: () => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
return self.form.find(':input[disabled]').enable();
}
});
})
.then(({ data }) => flash(data.message, 'notice'))
.then(() => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
self.form.find(':input[disabled]').enable();
})
.catch(error => flash(error.message));
}
setNewRepoCookie() {
......
......@@ -46,6 +46,11 @@
required: false,
default: '',
},
secondaryButtonLabel: {
type: String,
required: false,
default: '',
},
submitDisabled: {
type: Boolean,
required: false,
......@@ -129,6 +134,21 @@
>
{{ closeButtonLabel }}
</button>
<slot
v-if="secondaryButtonLabel"
name="secondary-button"
>
<button
v-if="secondaryButtonLabel"
type="button"
class="btn"
data-dismiss="modal"
>
{{ secondaryButtonLabel }}
</button>
</slot>
<button
v-if="primaryButtonLabel"
type="button"
......
......@@ -13,10 +13,7 @@ class RootController < Dashboard::ProjectsController
before_action :redirect_logged_user, if: -> { current_user.present? }
def index
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434
Gitlab::GitalyClient.allow_n_plus_1_calls do
super
end
super
end
private
......
......@@ -552,6 +552,7 @@ module Ci
variables = [
{ key: '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_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
......
......@@ -258,6 +258,10 @@ class Namespace < ActiveRecord::Base
all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
end
def features
[]
end
private
def path_or_parent_changed?
......
......@@ -5,7 +5,12 @@
%li.project-row{ class: ('no-description' if project.description.blank?) }
.controls
= 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
%span.badge
= storage_counter(project.statistics.storage_size)
......@@ -31,3 +36,5 @@
= paginate @projects, theme: 'gitlab'
- else
.nothing-here-block No projects found
#delete-project-modal
......@@ -44,12 +44,19 @@
%li.divider
- if user.can_be_removed?
%li
= link_to 'Remove user', admin_user_path(user),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" },
class: 'text-danger',
method: :delete
%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')
%li
= link_to 'Remove user and contributions', admin_user_path(user, hard_delete: true),
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?" },
class: 'text-danger',
method: :delete
%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')
......@@ -77,3 +77,6 @@
= render partial: 'admin/users/user', collection: @users
= paginate @users, theme: "gitlab"
#delete-user-modal
......@@ -176,13 +176,19 @@
.panel.panel-danger
.panel-heading
Remove user
= s_('AdminUsers|Delete user')
.panel-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user
%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
- if @user.solo_owned_groups.present?
%p
......@@ -196,7 +202,7 @@
.panel.panel-danger
.panel-heading
Remove user and contributions
= s_('AdminUsers|Delete user and contributions')
.panel-body
- if can?(current_user, :destroy_user, @user)
%p
......@@ -208,7 +214,15 @@
the user, and projects in them, will also be removed. Commits
to other projects are unaffected.
%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
%p
You don't have access to delete this user.
#delete-user-modal
......@@ -7,7 +7,7 @@
.repo-charts{ class: container_class }
%h4.sub-header
Programming languages used in this repository
= _("Programming languages used in this repository")
.row
.col-md-4
......@@ -30,9 +30,11 @@
.row.tree-ref-header
.col-md-6
%h4
Commit statistics for
%strong= @ref
#{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
- start_time = capture do
#{@commits_graph.start_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
.tree-ref-container
......@@ -45,32 +47,35 @@
.col-md-6
%ul.commit-stats
%li
Total:
%strong #{@commits_graph.commits.size} commits
- total = capture do
#{@commits_graph.commits.size}
= (_("Total: %{total}") % { total: "<strong>#{total} commits</strong>" }).html_safe
%li
Average per day:
%strong #{@commits_graph.commit_per_day} commits
- average = capture do
#{@commits_graph.commit_per_day}
= (_("Average per day: %{average}") % { average: "<strong>#{average} commits</strong>" }).html_safe
%li
Authors:
%strong= @commits_graph.authors
- authors = capture do
#{@commits_graph.authors}
= (_("Authors: %{authors}") % { authors: "<strong>#{authors}</strong>" }).html_safe
.col-md-6
%div
%p.slead
Commits per day of month
= _("Commits per day of month")
%canvas#month-chart
.row
.col-md-6
.col-md-6
%div
%p.slead
Commits per weekday
= _("Commits per weekday")
%canvas#weekday-chart
.row
.col-md-6
.col-md-6
%div
%p.slead
Commits per day hour (UTC)
= _("Commits per day hour (UTC)")
%canvas#hour-chart
%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
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
say 'Populating the `fork_networks` based on existing `forked_project_links`'
queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
say 'Fork networks will be populated in 20171205190711 - RescheduleForkNetworkCreationCaller'
end
def down
......
......@@ -3,22 +3,8 @@ class RescheduleForkNetworkCreation < ActiveRecord::Migration
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
say 'Populating the `fork_networks` based on existing `forked_project_links`'
queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
say 'Fork networks will be populated in 20171205190711 - RescheduleForkNetworkCreationCaller'
end
def down
......
......@@ -25,11 +25,11 @@ for each GitLab application server in your environment.
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/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/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-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,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/.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,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,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,nofail,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
......
......@@ -94,6 +94,18 @@ jobs = [['BackgroundMigrationClassName', [1]],
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
>**Note:**
......
......@@ -7,6 +7,8 @@ easy to maintain, and performant for the end-user.
### Naming
Filenames should use `snake_case`.
CSS classes should use the `lowercase-hyphenated` format rather than
`snake_case` or `camelCase`.
......
---
last_updated: 2017-10-05
last_updated: 2018-02-07
---
# 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.
## Prerequisites
## Omnibus installation
### Prerequisites
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
......@@ -19,7 +21,7 @@ Here's what you'll need to have installed:
- Omnibus GitLab
- MySQL
## Enable bundled PostgreSQL database
### Enable bundled PostgreSQL database
1. Stop GitLab:
......@@ -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.
## 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:
......@@ -104,122 +106,9 @@ the following:
----------------------------------------------- --------- --------- --------- --------------
public.abuse_reports 0 0 0 0.490s
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
----------------------------------------------- --------- --------- --------- --------------
COPY Threads Completion 4 4 0 2.008s
......@@ -240,9 +129,9 @@ the following:
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
a similar message to the following:
......@@ -256,7 +145,7 @@ debugger invoked on a CL-POSTGRES-ERROR:INSUFFICIENT-PRIVILEGE in thread
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:
......@@ -265,3 +154,130 @@ sudo gitlab-rake cache:clear
```
[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
- 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
......
......@@ -14,6 +14,14 @@ module Gitlab
def perform(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
INSERT INTO fork_network_members (fork_network_id, project_id, forked_from_project_id)
......@@ -33,10 +41,9 @@ module Gitlab
WHERE existing_members.project_id = forked_project_links.forked_to_project_id
)
INSERT_MEMBERS
if missing_members?(start_id, end_id)
BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [start_id, end_id])
end
rescue ActiveRecord::RecordNotUnique => e
# `fork_network_member` was created concurrently in another migration
log(e.message)
end
def missing_members?(start_id, end_id)
......
......@@ -53,11 +53,7 @@ module Gitlab
def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE)
Gitlab::GitalyClient.migrate(:list_blobs_by_sha_path) do |is_enabled|
if is_enabled
Gitlab::GitalyClient.allow_n_plus_1_calls do
blob_references.map do |sha, path|
find_by_gitaly(repository, sha, path, limit: blob_size_limit)
end
end
repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a
else
blob_references.map do |sha, path|
find_by_rugged(repository, sha, path, limit: blob_size_limit)
......
module Gitlab
module GitalyClient
class BlobService
include Gitlab::EncodingHelper
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
end
......@@ -54,6 +56,30 @@ module Gitlab
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
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
# add_namespace("/path/to/storage", "gitlab")
#
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
gitaly_namespace_client(storage).add(name)
else
......@@ -315,7 +316,8 @@ module Gitlab
# rm_namespace("/path/to/storage", "gitlab")
#
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
gitaly_namespace_client(storage).remove(name)
else
......@@ -333,7 +335,8 @@ module Gitlab
# mv_namespace("/path/to/storage", "gitlab", "gitlabhq")
#
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
gitaly_namespace_client(storage).rename(old_name, new_name)
else
......@@ -368,7 +371,8 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
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
gitaly_namespace_client(storage).exists?(dir_name)
else
......
......@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-07 13:35+0100\n"
"PO-Revision-Date: 2018-02-07 13:35+0100\n"
"POT-Creation-Date: 2018-02-07 11:38-0600\n"
"PO-Revision-Date: 2018-02-07 11:38-0600\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -279,6 +279,9 @@ msgstr ""
msgid "Author"
msgstr ""
msgid "Authors: %{authors}"
msgstr ""
msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
msgstr ""
......@@ -312,6 +315,7 @@ msgstr ""
msgid "Avatar will be removed. Are you sure?"
msgstr ""
<<<<<<< HEAD
msgid "Billing"
msgstr ""
......@@ -364,6 +368,9 @@ msgid "BillingPlans|paid annually at %{price_per_year}"
msgstr ""
msgid "BillingPlans|per user"
=======
msgid "Average per day: %{average}"
>>>>>>> upstream/master
msgstr ""
msgid "Begin with the selected commit"
......@@ -830,6 +837,9 @@ msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
msgstr ""
msgid "ClusterIntegration|Manage"
msgstr ""
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
msgstr ""
......@@ -970,6 +980,9 @@ msgstr ""
msgid "Commit message"
msgstr ""
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
msgstr ""
msgid "CommitBoxTitle|Commit"
msgstr ""
......@@ -982,6 +995,15 @@ msgstr ""
msgid "Commits feed"
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."
msgstr ""
......@@ -1490,6 +1512,7 @@ msgstr ""
msgid "Generate a default set of labels"
msgstr ""
<<<<<<< HEAD
msgid "Geo Nodes"
msgstr ""
......@@ -1604,6 +1627,8 @@ msgstr ""
msgid "Geo|Shards to synchronize"
msgstr ""
=======
>>>>>>> upstream/master
msgid "Git revision"
msgstr ""
......@@ -2368,6 +2393,9 @@ msgstr ""
msgid "Profiles|your account"
msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
......@@ -3338,10 +3366,14 @@ msgstr ""
msgid "Total test time for all commits/merges"
msgstr ""
<<<<<<< HEAD
msgid "Track activity with Contribution Analytics."
msgstr ""
msgid "Track groups of issues that share a theme, across projects and milestones"
=======
msgid "Total: %{total}"
>>>>>>> upstream/master
msgstr ""
msgid "Track time with quick actions"
......@@ -3590,6 +3622,9 @@ msgstr ""
msgid "You can move around the graph by using the arrow keys."
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"
msgstr ""
......
......@@ -26,8 +26,8 @@ describe "Admin::Users" do
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
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_link('Remove user and contributions', href: admin_user_path(user, hard_delete: true))
expect(page).to have_button('Delete user')
expect(page).to have_button('Delete user and contributions')
end
describe 'Two-factor Authentication filters' do
......@@ -122,8 +122,8 @@ describe "Admin::Users" do
expect(page).to have_content(user.email)
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('Remove user', href: admin_user_path(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')
expect(page).to have_button('Delete user and contributions')
end
describe 'Impersonation' do
......
......@@ -12,4 +12,13 @@ describe 'User visits their profile' do
it 'shows correct menu item' do
expect(page).to have_active_navigation('Profile')
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
......@@ -109,7 +109,8 @@ describe 'Pipelines', :js do
context 'when canceling' 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
end
......@@ -140,6 +141,7 @@ describe 'Pipelines', :js do
context 'when retrying' do
before do
find('.js-pipelines-retry-button').click
find('.js-primary-button').click
wait_for_requests
end
......@@ -238,7 +240,8 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
accept_alert { find('.js-pipelines-cancel-button').click }
find('.js-pipelines-cancel-button').click
find('.js-primary-button').click
end
it 'indicates that pipeline was canceled' do
......
......@@ -268,10 +268,10 @@ describe('AppComponent', () => {
it('updates props which show modal confirmation dialog', () => {
const group = Object.assign({}, mockParentGroupItem);
expect(vm.showModal).toBeFalsy();
expect(vm.updateModal).toBeFalsy();
expect(vm.groupLeaveConfirmationMessage).toBe('');
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?`);
});
});
......@@ -280,9 +280,9 @@ describe('AppComponent', () => {
it('hides modal confirmation which is shown before leaving the group', () => {
const group = Object.assign({}, mockParentGroupItem);
vm.showLeaveGroupModal(group, mockParentGroupItem);
expect(vm.showModal).toBeTruthy();
expect(vm.updateModal).toBeTruthy();
vm.hideLeaveGroupModal();
expect(vm.showModal).toBeFalsy();
expect(vm.updateModal).toBeFalsy();
});
});
......@@ -307,7 +307,7 @@ describe('AppComponent', () => {
spyOn($, 'scrollTo');
vm.leaveGroup();
expect(vm.showModal).toBeFalsy();
expect(vm.updateModal).toBeFalsy();
expect(vm.targetGroup.isBeingRemoved).toBeTruthy();
expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath);
setTimeout(() => {
......@@ -475,7 +475,7 @@ describe('AppComponent', () => {
it('renders modal confirmation dialog', () => {
vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?';
vm.showModal = true;
vm.updateModal = true;
const modalDialogEl = vm.$el.querySelector('.modal');
expect(modalDialogEl).not.toBe(null);
expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?');
......
......@@ -2,14 +2,29 @@ import _ from 'underscore';
import Vue from 'vue';
import notesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service';
import '~/render_gfm';
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', () => {
let mountComponent;
let vm;
beforeEach(() => {
jasmine.addMatchers(vueMatchers);
const IssueNotesApp = Vue.extend(notesApp);
mountComponent = (data) => {
......@@ -105,7 +120,7 @@ describe('note_app', () => {
});
it('should render loading icon', () => {
expect(vm.$el.querySelector('.js-loading')).toBeDefined();
expect(vm).toIncludeElement('.js-loading');
});
it('should render form', () => {
......@@ -118,10 +133,14 @@ describe('note_app', () => {
describe('update note', () => {
describe('individual note', () => {
beforeEach(() => {
beforeEach((done) => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent();
setTimeout(() => {
vm.$el.querySelector('.js-note-edit').click();
Vue.nextTick(done);
}, 0);
});
afterEach(() => {
......@@ -131,40 +150,32 @@ describe('note_app', () => {
);
});
it('renders edit form', (done) => {
setTimeout(() => {
vm.$el.querySelector('.js-note-edit').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined();
done();
});
}, 0);
it('renders edit form', () => {
expect(vm).toIncludeElement('.js-vue-issue-note-form');
});
it('calls the service to update the note', (done) => {
getSetTimeoutPromise()
.then(() => {
vm.$el.querySelector('.js-note-edit').click();
})
.then(Vue.nextTick)
.then(() => {
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)
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
Vue.nextTick()
.then(done)
.catch(done.fail);
});
});
describe('dicussion note', () => {
beforeEach(() => {
describe('discussion note', () => {
beforeEach((done) => {
Vue.http.interceptors.push(mockData.discussionNoteInterceptor);
spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent();
setTimeout(() => {
vm.$el.querySelector('.js-note-edit').click();
Vue.nextTick(done);
}, 0);
});
afterEach(() => {
......@@ -174,30 +185,17 @@ describe('note_app', () => {
);
});
it('renders edit form', (done) => {
setTimeout(() => {
vm.$el.querySelector('.js-note-edit').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined();
done();
});
}, 0);
it('renders edit form', () => {
expect(vm).toIncludeElement('.js-vue-issue-note-form');
});
it('updates the note and resets the edit form', (done) => {
getSetTimeoutPromise()
.then(() => {
vm.$el.querySelector('.js-note-edit').click();
})
.then(Vue.nextTick)
.then(() => {
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)
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
Vue.nextTick()
.then(done)
.catch(done.fail);
});
......
......@@ -56,4 +56,25 @@ describe('issue_note', () => {
done();
}, 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', () => {
title: 'Foo',
icon: 'repeat',
cssClass: 'bar',
id: 123,
},
}).$mount();
});
......@@ -38,9 +39,8 @@ describe('Pipelines Async Button', () => {
describe('With confirm dialog', () => {
it('should call the service when confimation is positive', () => {
spyOn(window, 'confirm').and.returnValue(true);
eventHub.$on('postAction', (endpoint) => {
expect(endpoint).toEqual('/foo');
eventHub.$on('actionConfirmationModal', (data) => {
expect(data.id).toEqual(123);
});
component = new AsyncButtonComponent({
......@@ -49,7 +49,7 @@ describe('Pipelines Async Button', () => {
title: 'Foo',
icon: 'fa fa-foo',
cssClass: 'bar',
confirmActionMessage: 'bar',
id: 123,
},
}).$mount();
......
......@@ -178,67 +178,77 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
describe '.batch' do
let(:blob_references) do
[
[SeedRepo::Commit::ID, "files/ruby/popen.rb"],
[SeedRepo::Commit::ID, 'six']
]
end
shared_examples 'loading blobs in batch' do
let(:blob_references) do
[
[SeedRepo::Commit::ID, "files/ruby/popen.rb"],
[SeedRepo::Commit::ID, 'six']
]
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
let(:blob) { subject[0] }
context 'first blob' do
let(:blob) { subject[0] }
it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
it { expect(blob.path).to eq("files/ruby/popen.rb") }
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.size).to eq(669) }
it { expect(blob.mode).to eq("100644") }
end
it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
it { expect(blob.path).to eq("files/ruby/popen.rb") }
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.size).to eq(669) }
it { expect(blob.mode).to eq("100644") }
end
context 'second blob' do
let(:blob) { subject[1] }
context 'second blob' do
let(:blob) { subject[1] }
it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
it { expect(blob.data).to eq('') }
it 'does not mark the blob as binary' do
expect(blob).not_to be_binary
it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
it { expect(blob.data).to eq('') }
it 'does not mark the blob as binary' do
expect(blob).not_to be_binary
end
end
end
context 'limiting' do
subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) }
context 'limiting' do
subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) }
context 'positive' do
let(:blob_size_limit) { 10 }
context 'positive' do
let(:blob_size_limit) { 10 }
it { expect(subject.first.data.size).to eq(10) }
end
it { expect(subject.first.data.size).to eq(10) }
end
context 'zero' do
let(:blob_size_limit) { 0 }
context 'zero' do
let(:blob_size_limit) { 0 }
it 'only loads the metadata' do
expect(subject.first.size).not_to be(0)
expect(subject.first.data).to eq('')
it 'only loads the metadata' do
expect(subject.first.size).not_to be(0)
expect(subject.first.data).to eq('')
end
end
end
context 'negative' do
let(:blob_size_limit) { -1 }
context 'negative' do
let(:blob_size_limit) { -1 }
it 'ignores MAX_DATA_DISPLAY_SIZE' do
stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100)
it 'ignores MAX_DATA_DISPLAY_SIZE' do
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
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
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
[
{ key: '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_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
......
......@@ -103,6 +103,7 @@ performance:
artifacts:
paths:
- performance.json
- sitespeed-results/
only:
refs:
- branches
......@@ -503,16 +504,16 @@ production:
export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
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
if [ -f .gitlab-urls.txt ]
then
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
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
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