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`.
......
This diff is collapsed.
# 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