Commit a5605e3a authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into 'pages-0-7-0'

 Conflicts:
   GITLAB_PAGES_VERSION
parents 9a883f15 98c8c90e
...@@ -2,6 +2,13 @@ ...@@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.5.3 (2018-03-01)
### Security (1 change)
- Ensure that OTP backup codes are always invalidated.
## 10.5.2 (2018-02-25) ## 10.5.2 (2018-02-25)
### Fixed (7 changes) ### Fixed (7 changes)
...@@ -219,6 +226,13 @@ entry. ...@@ -219,6 +226,13 @@ entry.
- Adds empty state illustration for pending job. - Adds empty state illustration for pending job.
## 10.4.5 (2018-03-01)
### Security (1 change)
- Ensure that OTP backup codes are always invalidated.
## 10.4.4 (2018-02-16) ## 10.4.4 (2018-02-16)
### Security (1 change) ### Security (1 change)
...@@ -443,6 +457,13 @@ entry. ...@@ -443,6 +457,13 @@ entry.
- Use a background migration for issues.closed_at. - Use a background migration for issues.closed_at.
## 10.3.8 (2018-03-01)
### Security (1 change)
- Ensure that OTP backup codes are always invalidated.
## 10.3.7 (2018-02-05) ## 10.3.7 (2018-02-05)
### Security (4 changes) ### Security (4 changes)
......
<script>
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
export default {
components: {
GlModal,
},
props: {
milestoneTitle: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
},
computed: {
title() {
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
},
text() {
return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group.
Existing project milestones with the same title will be merged.
This action cannot be reversed.`);
},
},
methods: {
onSubmit() {
eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
return axios.post(this.url, { params: { format: 'json' } })
.then((response) => {
eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true });
visitUrl(response.data.url);
})
.catch((error) => {
eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false });
createFlash(error);
});
},
},
};
</script>
<template>
<gl-modal
id="promote-milestone-modal"
footer-primary-button-variant="warning"
:footer-primary-button-text="s__('Milestones|Promote Milestone')"
@submit="onSubmit"
>
<template
slot="title"
>
{{ title }}
</template>
{{ text }}
</gl-modal>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import deleteMilestoneModal from './components/delete_milestone_modal.vue';
import eventHub from './event_hub';
export default () => {
Vue.use(Translate);
const onRequestFinished = ({ milestoneUrl, successful }) => {
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
if (!successful) {
button.removeAttribute('disabled');
}
button.querySelector('.js-loading-icon').classList.add('hidden');
};
const onRequestStarted = (milestoneUrl) => {
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
button.setAttribute('disabled', '');
button.querySelector('.js-loading-icon').classList.remove('hidden');
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
};
const onDeleteButtonClick = (event) => {
const button = event.currentTarget;
const modalProps = {
milestoneId: parseInt(button.dataset.milestoneId, 10),
milestoneTitle: button.dataset.milestoneTitle,
milestoneUrl: button.dataset.milestoneUrl,
issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
};
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('deleteMilestoneModal.props', modalProps);
};
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
deleteMilestoneButtons.forEach((button) => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('deleteMilestoneModal.mounted', () => {
deleteMilestoneButtons.forEach((button) => {
button.removeAttribute('disabled');
});
});
return new Vue({
el: '#delete-milestone-modal',
components: {
deleteMilestoneModal,
},
data() {
return {
modalProps: {
milestoneId: -1,
milestoneTitle: '',
milestoneUrl: '',
issueCount: -1,
mergeRequestCount: -1,
},
};
},
mounted() {
eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
eventHub.$emit('deleteMilestoneModal.mounted');
},
beforeDestroy() {
eventHub.$off('deleteMilestoneModal.props', this.setModalProps);
},
methods: {
setModalProps(modalProps) {
this.modalProps = modalProps;
},
},
render(createElement) {
return createElement(deleteMilestoneModal, {
props: this.modalProps,
});
},
});
};
import Vue from 'vue'; import initDeleteMilestoneModal from './delete_milestone_modal_init';
import initPromoteMilestoneModal from './promote_milestone_modal_init';
import Translate from '~/vue_shared/translate';
import deleteMilestoneModal from './components/delete_milestone_modal.vue';
import eventHub from './event_hub';
export default () => { export default () => {
Vue.use(Translate); initDeleteMilestoneModal();
initPromoteMilestoneModal();
const onRequestFinished = ({ milestoneUrl, successful }) => {
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
if (!successful) {
button.removeAttribute('disabled');
}
button.querySelector('.js-loading-icon').classList.add('hidden');
};
const onRequestStarted = (milestoneUrl) => {
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
button.setAttribute('disabled', '');
button.querySelector('.js-loading-icon').classList.remove('hidden');
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
};
const onDeleteButtonClick = (event) => {
const button = event.currentTarget;
const modalProps = {
milestoneId: parseInt(button.dataset.milestoneId, 10),
milestoneTitle: button.dataset.milestoneTitle,
milestoneUrl: button.dataset.milestoneUrl,
issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
};
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('deleteMilestoneModal.props', modalProps);
};
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
const button = deleteMilestoneButtons[i];
button.addEventListener('click', onDeleteButtonClick);
}
eventHub.$once('deleteMilestoneModal.mounted', () => {
for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
const button = deleteMilestoneButtons[i];
button.removeAttribute('disabled');
}
});
return new Vue({
el: '#delete-milestone-modal',
components: {
deleteMilestoneModal,
},
data() {
return {
modalProps: {
milestoneId: -1,
milestoneTitle: '',
milestoneUrl: '',
issueCount: -1,
mergeRequestCount: -1,
},
};
},
mounted() {
eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
eventHub.$emit('deleteMilestoneModal.mounted');
},
beforeDestroy() {
eventHub.$off('deleteMilestoneModal.props', this.setModalProps);
},
methods: {
setModalProps(modalProps) {
this.modalProps = modalProps;
},
},
render(createElement) {
return createElement(deleteMilestoneModal, {
props: this.modalProps,
});
},
});
}; };
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
import eventHub from './event_hub';
Vue.use(Translate);
export default () => {
const onRequestFinished = ({ milestoneUrl, successful }) => {
const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`);
if (!successful) {
button.removeAttribute('disabled');
}
};
const onRequestStarted = (milestoneUrl) => {
const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`);
button.setAttribute('disabled', '');
eventHub.$once('promoteMilestoneModal.requestFinished', onRequestFinished);
};
const onDeleteButtonClick = (event) => {
const button = event.currentTarget;
const modalProps = {
milestoneTitle: button.dataset.milestoneTitle,
url: button.dataset.url,
};
eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteMilestoneModal.props', modalProps);
};
const promoteMilestoneButtons = document.querySelectorAll('.js-promote-project-milestone-button');
promoteMilestoneButtons.forEach((button) => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('promoteMilestoneModal.mounted', () => {
promoteMilestoneButtons.forEach((button) => {
button.removeAttribute('disabled');
});
});
const promoteMilestoneModal = document.getElementById('promote-milestone-modal');
let promoteMilestoneComponent;
if (promoteMilestoneModal) {
promoteMilestoneComponent = new Vue({
el: promoteMilestoneModal,
components: {
PromoteMilestoneModal,
},
data() {
return {
modalProps: {
milestoneTitle: '',
url: '',
},
};
},
mounted() {
eventHub.$on('promoteMilestoneModal.props', this.setModalProps);
eventHub.$emit('promoteMilestoneModal.mounted');
},
beforeDestroy() {
eventHub.$off('promoteMilestoneModal.props', this.setModalProps);
},
methods: {
setModalProps(modalProps) {
this.modalProps = modalProps;
},
},
render(createElement) {
return createElement('promote-milestone-modal', {
props: this.modalProps,
});
},
});
}
return promoteMilestoneComponent;
};
<script>
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
export default {
components: {
GlModal,
},
props: {
url: {
type: String,
required: true,
},
labelTitle: {
type: String,
required: true,
},
labelColor: {
type: String,
required: true,
},
labelTextColor: {
type: String,
required: true,
},
},
computed: {
text() {
return s__(`Milestones|Promoting this label will make it available for all projects inside the group.
Existing project labels with the same title will be merged. This action cannot be reversed.`);
},
title() {
const label = `<span
class="label color-label"
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
>${this.labelTitle}</span>`;
return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), {
labelTitle: label,
}, false);
},
},
methods: {
onSubmit() {
eventHub.$emit('promoteLabelModal.requestStarted', this.url);
return axios.post(this.url, { params: { format: 'json' } })
.then((response) => {
eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: true });
visitUrl(response.data.url);
})
.catch((error) => {
eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: false });
createFlash(error);
});
},
},
};
</script>
<template>
<gl-modal
id="promote-label-modal"
footer-primary-button-variant="warning"
:footer-primary-button-text="s__('Labels|Promote Label')"
@submit="onSubmit"
>
<div
slot="title"
v-html="title"
>
{{ title }}
</div>
{{ text }}
</gl-modal>
</template>
import Vue from 'vue';
export default new Vue();
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import initLabels from '~/init_labels'; import initLabels from '~/init_labels';
import eventHub from '../event_hub';
import PromoteLabelModal from '../components/promote_label_modal.vue';
document.addEventListener('DOMContentLoaded', initLabels); Vue.use(Translate);
const initLabelIndex = () => {
initLabels();
const onRequestFinished = ({ labelUrl, successful }) => {
const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
if (!successful) {
button.removeAttribute('disabled');
}
};
const onRequestStarted = (labelUrl) => {
const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
button.setAttribute('disabled', '');
eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished);
};
const onDeleteButtonClick = (event) => {
const button = event.currentTarget;
const modalProps = {
labelTitle: button.dataset.labelTitle,
labelColor: button.dataset.labelColor,
labelTextColor: button.dataset.labelTextColor,
url: button.dataset.url,
};
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteLabelModal.props', modalProps);
};
const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button');
promoteLabelButtons.forEach((button) => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('promoteLabelModal.mounted', () => {
promoteLabelButtons.forEach((button) => {
button.removeAttribute('disabled');
});
});
const promoteLabelModal = document.getElementById('promote-label-modal');
let promoteLabelModalComponent;
if (promoteLabelModal) {
promoteLabelModalComponent = new Vue({
el: promoteLabelModal,
components: {
PromoteLabelModal,
},
data() {
return {
modalProps: {
labelTitle: '',
labelColor: '',
labelTextColor: '',
url: '',
},
};
},
mounted() {
eventHub.$on('promoteLabelModal.props', this.setModalProps);
eventHub.$emit('promoteLabelModal.mounted');
},
beforeDestroy() {
eventHub.$off('promoteLabelModal.props', this.setModalProps);
},
methods: {
setModalProps(modalProps) {
this.modalProps = modalProps;
},
},
render(createElement) {
return createElement('promote-label-modal', {
props: this.modalProps,
});
},
});
}
return promoteLabelModalComponent;
};
document.addEventListener('DOMContentLoaded', initLabelIndex);
...@@ -2,14 +2,17 @@ ...@@ -2,14 +2,17 @@
background-color: $modal-body-bg; background-color: $modal-body-bg;
padding: #{3 * $grid-size} #{2 * $grid-size}; padding: #{3 * $grid-size} #{2 * $grid-size};
.page-title { .page-title,
margin-top: 0; .modal-title {
.color-label { .color-label {
font-size: $gl-font-size; font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal; padding: $gl-vert-padding $label-padding-modal;
} }
} }
.page-title {
margin-top: 0;
}
} }
.modal-body { .modal-body {
......
...@@ -56,6 +56,7 @@ module AuthenticatesWithTwoFactor ...@@ -56,6 +56,7 @@ module AuthenticatesWithTwoFactor
session.delete(:otp_user_id) session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
user.save!
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts! user.increment_failed_attempts!
......
...@@ -112,12 +112,14 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -112,12 +112,14 @@ class Projects::LabelsController < Projects::ApplicationController
begin begin
return render_404 unless promote_service.execute(@label) return render_404 unless promote_service.execute(@label)
flash[:notice] = "#{@label.title} promoted to group label."
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to(project_labels_path(@project), redirect_to(project_labels_path(@project), status: 303)
notice: 'Label was promoted to a Group Label') end
format.json do
render json: { url: project_labels_path(@project) }
end end
format.js
end end
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label" Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label"
......
...@@ -70,9 +70,17 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -70,9 +70,17 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
def promote def promote
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone) Milestones::PromoteService.new(project, current_user).execute(milestone)
flash[:notice] = "Milestone has been promoted to group milestone."
redirect_to group_milestone_path(project.group, promoted_milestone.iid) flash[:notice] = "#{milestone.title} promoted to group milestone"
respond_to do |format|
format.html do
redirect_to project_milestones_path(project)
end
format.json do
render json: { url: project_milestones_path(project) }
end
end
rescue Milestones::PromoteService::PromoteMilestoneError => error rescue Milestones::PromoteService::PromoteMilestoneError => error
redirect_to milestone, alert: error.message redirect_to milestone, alert: error.message
end end
......
...@@ -117,7 +117,7 @@ class Notify < BaseMailer ...@@ -117,7 +117,7 @@ class Notify < BaseMailer
if Gitlab::IncomingEmail.enabled? && @sent_notification if Gitlab::IncomingEmail.enabled? && @sent_notification
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace address.display_name = @project.full_name
headers['Reply-To'] = address headers['Reply-To'] = address
......
...@@ -85,7 +85,7 @@ module Projects ...@@ -85,7 +85,7 @@ module Projects
end end
def after_create_actions def after_create_actions
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") log_info("#{@project.owner.name} created a new project \"#{@project.full_name}\"")
unless @project.gitlab_project_import? unless @project.gitlab_project_import?
@project.write_repository_config @project.write_repository_config
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
- can_admin_label = can?(current_user, :admin_label, @project) - can_admin_label = can?(current_user, :admin_label, @project)
- if @labels.exists? || @prioritized_labels.exists? - if @labels.exists? || @prioritized_labels.exists?
#promote-label-modal
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust .top-area.adjust
.nav-text .nav-text
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
.milestones .milestones
#delete-milestone-modal #delete-milestone-modal
#promote-milestone-modal
%ul.content-list %ul.content-list
= render @milestones = render @milestones
......
...@@ -27,8 +27,15 @@ ...@@ -27,8 +27,15 @@
Edit Edit
- if @project.group - if @project.group
= link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting #{@milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do %button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
Promote target: '#promote-milestone-modal',
milestone_title: @milestone.title,
url: promote_project_milestone_path(@milestone.project, @milestone),
container: 'body' },
disabled: true,
type: 'button' }
= _('Promote')
#promote-milestone-modal
- if @milestone.active? - if @milestone.active?
= link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" = link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
......
...@@ -48,8 +48,16 @@ ...@@ -48,8 +48,16 @@
.pull-right.hidden-xs.hidden-sm .pull-right.hidden-xs.hidden-sm
- if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
= link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting #{label.title} will make it available for all projects inside #{label.project.group.name}. Existing project labels with the same name will be merged. This action cannot be reversed.", toggle: "tooltip"}, method: :post do %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
%span.sr-only Promote to Group disabled: true,
type: 'button',
data: { url: promote_project_label_path(label.project, label),
label_title: label.title,
label_color: label.color,
label_text_color: label.text_color,
target: '#promote-label-modal',
container: 'body',
toggle: 'modal' } }
= sprite_icon('level-up') = sprite_icon('level-up')
- if can?(current_user, :admin_label, label) - if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
......
...@@ -51,8 +51,15 @@ ...@@ -51,8 +51,15 @@
\ \
- if @project.group - if @project.group
= link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting #{milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do %button.js-promote-project-milestone-button.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
Promote disabled: true,
type: 'button',
data: { url: promote_project_milestone_path(milestone.project, milestone),
milestone_title: milestone.title,
target: '#promote-milestone-modal',
container: 'body',
toggle: 'modal' } }
= _('Promote')
= link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped" = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
......
...@@ -66,7 +66,7 @@ class EmailsOnPushWorker ...@@ -66,7 +66,7 @@ class EmailsOnPushWorker
# These are input errors and won't be corrected even if Sidekiq retries # These are input errors and won't be corrected even if Sidekiq retries
rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}") logger.info("Failed to send e-mail for project '#{project.full_name}' to #{recipient}: #{e}")
end end
end end
ensure ensure
......
---
title: Added new design for promotion modals
merge_request: 17197
author:
type: other
---
title: Ensure that OTP backup codes are always invalidated
merge_request:
author:
type: security
---
title: Upgrade GitLab Workhorse to 4.0.0
merge_request:
author:
type: added
...@@ -37,7 +37,7 @@ following locations: ...@@ -37,7 +37,7 @@ following locations:
- [Group milestones](group_milestones.md) - [Group milestones](group_milestones.md)
- [Namespaces](namespaces.md) - [Namespaces](namespaces.md)
- [Notes](notes.md) (comments) - [Notes](notes.md) (comments)
- [Threaded comments](discussions.md) - [Discussions](discussions.md) (threaded comments)
- [Notification settings](notification_settings.md) - [Notification settings](notification_settings.md)
- [Open source license templates](templates/licenses.md) - [Open source license templates](templates/licenses.md)
- [Pages Domains](pages_domains.md) - [Pages Domains](pages_domains.md)
......
# Discussions API # Discussions API
Discussions are set of related notes on snippets, issues or epics. Discussions are set of related notes on snippets or issues.
## Issues ## Issues
......
...@@ -163,8 +163,7 @@ in your `.gitlab-ci.yml`. ...@@ -163,8 +163,7 @@ in your `.gitlab-ci.yml`.
Behind the scenes, this works by increasing a counter in the database, and the Behind the scenes, this works by increasing a counter in the database, and the
value of that counter is used to create the key for the cache. After a push, a value of that counter is used to create the key for the cache. After a push, a
new key is generated and the old cache is not valid anymore. Eventually, the new key is generated and the old cache is not valid anymore.
Runner's garbage collector will remove it form the filesystem.
## How shared Runners pick jobs ## How shared Runners pick jobs
......
...@@ -82,7 +82,7 @@ module SharedProject ...@@ -82,7 +82,7 @@ module SharedProject
step 'I should see project "Shop" activity feed' do step 'I should see project "Shop" activity feed' do
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
expect(page).to have_content "#{@user.name} pushed new branch fix at #{project.name_with_namespace}" expect(page).to have_content "#{@user.name} pushed new branch fix at #{project.full_name}"
end end
step 'I should see project settings' do step 'I should see project settings' do
...@@ -113,12 +113,12 @@ module SharedProject ...@@ -113,12 +113,12 @@ module SharedProject
step 'I should not see project "Archive"' do step 'I should not see project "Archive"' do
project = Project.find_by(name: "Archive") project = Project.find_by(name: "Archive")
expect(page).not_to have_content project.name_with_namespace expect(page).not_to have_content project.full_name
end end
step 'I should see project "Archive"' do step 'I should see project "Archive"' do
project = Project.find_by(name: "Archive") project = Project.find_by(name: "Archive")
expect(page).to have_content project.name_with_namespace expect(page).to have_content project.full_name
end end
# ---------------------------------------- # ----------------------------------------
......
...@@ -111,13 +111,6 @@ module API ...@@ -111,13 +111,6 @@ module API
def gitaly_payload(action) def gitaly_payload(action)
return unless %w[git-receive-pack git-upload-pack].include?(action) return unless %w[git-receive-pack git-upload-pack].include?(action)
if action == 'git-receive-pack'
return unless Gitlab::GitalyClient.feature_enabled?(
:ssh_receive_pack,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT
)
end
{ {
repository: repository.gitaly_repository, repository: repository.gitaly_repository,
address: Gitlab::GitalyClient.address(project.repository_storage), address: Gitlab::GitalyClient.address(project.repository_storage),
......
...@@ -135,7 +135,7 @@ module Gitlab ...@@ -135,7 +135,7 @@ module Gitlab
if label.valid? if label.valid?
@labels[label_params[:title]] = label @labels[label_params[:title]] = label
else else
raise "Failed to create label \"#{label_params[:title]}\" for project \"#{project.name_with_namespace}\"" raise "Failed to create label \"#{label_params[:title]}\" for project \"#{project.full_name}\""
end end
end end
end end
......
...@@ -31,7 +31,7 @@ module Gitlab ...@@ -31,7 +31,7 @@ module Gitlab
# TODO: do we still need it? # TODO: do we still need it?
project_id: project.id, project_id: project.id,
project_name: project.name_with_namespace, project_name: project.full_name,
user: { user: {
id: user.try(:id), id: user.try(:id),
......
...@@ -38,7 +38,7 @@ module Gitlab ...@@ -38,7 +38,7 @@ module Gitlab
end end
def project_link def project_link
"[#{project.name_with_namespace}](#{project.web_url})" "[#{project.full_name}](#{project.web_url})"
end end
def author_profile_link def author_profile_link
......
...@@ -53,7 +53,7 @@ module Gitlab ...@@ -53,7 +53,7 @@ module Gitlab
end end
def pretext def pretext
"Issue *#{@resource.to_reference}* from #{project.name_with_namespace}" "Issue *#{@resource.to_reference}* from #{project.full_name}"
end end
end end
end end
......
...@@ -10,6 +10,7 @@ module Gitlab ...@@ -10,6 +10,7 @@ module Gitlab
INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze
INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze
NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze
ALLOWED_GIT_HTTP_ACTIONS = %w[git_receive_pack git_upload_pack info_refs].freeze
# Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32 # Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
# bytes https://tools.ietf.org/html/rfc4868#section-2.6 # bytes https://tools.ietf.org/html/rfc4868#section-2.6
...@@ -17,6 +18,8 @@ module Gitlab ...@@ -17,6 +18,8 @@ module Gitlab
class << self class << self
def git_http_ok(repository, is_wiki, user, action, show_all_refs: false) def git_http_ok(repository, is_wiki, user, action, show_all_refs: false)
raise "Unsupported action: #{action}" unless ALLOWED_GIT_HTTP_ACTIONS.include?(action.to_s)
project = repository.project project = repository.project
repo_path = repository.path_to_repo repo_path = repository.path_to_repo
params = { params = {
...@@ -31,24 +34,7 @@ module Gitlab ...@@ -31,24 +34,7 @@ module Gitlab
token: Gitlab::GitalyClient.token(project.repository_storage) token: Gitlab::GitalyClient.token(project.repository_storage)
} }
params[:Repository] = repository.gitaly_repository.to_h params[:Repository] = repository.gitaly_repository.to_h
params[:GitalyServer] = server
feature_enabled = case action.to_s
when 'git_receive_pack'
Gitlab::GitalyClient.feature_enabled?(
:post_receive_pack,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT
)
when 'git_upload_pack'
true
when 'info_refs'
true
else
raise "Unsupported action: #{action}"
end
if feature_enabled
params[:GitalyServer] = server
end
params params
end end
......
...@@ -50,7 +50,7 @@ module SystemCheck ...@@ -50,7 +50,7 @@ module SystemCheck
if should_sanitize? if should_sanitize?
"#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... " "#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... "
else else
"#{project.name_with_namespace.color(:yellow)} ... " "#{project.full_name.color(:yellow)} ... "
end end
end end
......
...@@ -98,10 +98,8 @@ describe Projects::MilestonesController do ...@@ -98,10 +98,8 @@ describe Projects::MilestonesController do
it 'shows group milestone' do it 'shows group milestone' do
post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
group_milestone = assigns(:milestone) expect(flash[:notice]).to eq("#{milestone.title} promoted to group milestone")
expect(response).to redirect_to(project_milestones_path(project))
expect(response).to redirect_to(group_milestone_path(project.group, group_milestone.iid))
expect(flash[:notice]).to eq('Milestone has been promoted to group milestone.')
end end
end end
......
...@@ -145,6 +145,18 @@ feature 'Login' do ...@@ -145,6 +145,18 @@ feature 'Login' do
expect { enter_code(codes.sample) } expect { enter_code(codes.sample) }
.to change { user.reload.otp_backup_codes.size }.by(-1) .to change { user.reload.otp_backup_codes.size }.by(-1)
end end
it 'invalidates backup codes twice in a row' do
random_code = codes.delete(codes.sample)
expect { enter_code(random_code) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
gitlab_sign_out
gitlab_sign_in(user)
expect { enter_code(codes.sample) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
end
end end
context 'with invalid code' do context 'with invalid code' do
......
import Vue from 'vue';
import promoteLabelModal from '~/pages/projects/labels/components/promote_label_modal.vue';
import eventHub from '~/pages/projects/labels/event_hub';
import axios from '~/lib/utils/axios_utils';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('Promote label modal', () => {
let vm;
const Component = Vue.extend(promoteLabelModal);
const labelMockData = {
labelTitle: 'Documentation',
labelColor: '#5cb85c',
labelTextColor: '#ffffff',
url: `${gl.TEST_HOST}/dummy/promote/labels`,
};
describe('Modal title and description', () => {
beforeEach(() => {
vm = mountComponent(Component, labelMockData);
});
afterEach(() => {
vm.$destroy();
});
it('contains the proper description', () => {
expect(vm.text).toContain('Promoting this label will make it available for all projects inside the group');
});
it('contains a label span with the color', () => {
const labelFromTitle = vm.$el.querySelector('.modal-header .label.color-label');
expect(labelFromTitle.style.backgroundColor).not.toBe(null);
expect(labelFromTitle.textContent).toContain(vm.labelTitle);
});
});
describe('When requesting a label promotion', () => {
beforeEach(() => {
vm = mountComponent(Component, {
...labelMockData,
});
spyOn(eventHub, '$emit');
});
afterEach(() => {
vm.$destroy();
});
it('redirects when a label is promoted', (done) => {
const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(labelMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestStarted', labelMockData.url);
return Promise.resolve({
request: {
responseURL,
},
});
});
vm.onSubmit()
.then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { labelUrl: labelMockData.url, successful: true });
})
.then(done)
.catch(done.fail);
});
it('displays an error if promoting a label failed', (done) => {
const dummyError = new Error('promoting label failed');
dummyError.response = { status: 500 };
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(labelMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestStarted', labelMockData.url);
return Promise.reject(dummyError);
});
vm.onSubmit()
.catch((error) => {
expect(error).toBe(dummyError);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { labelUrl: labelMockData.url, successful: false });
})
.then(done)
.catch(done.fail);
});
});
});
import Vue from 'vue';
import promoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
import axios from '~/lib/utils/axios_utils';
import mountComponent from '../../../../helpers/vue_mount_component_helper';
describe('Promote milestone modal', () => {
let vm;
const Component = Vue.extend(promoteMilestoneModal);
const milestoneMockData = {
milestoneTitle: 'v1.0',
url: `${gl.TEST_HOST}/dummy/promote/milestones`,
};
describe('Modal title and description', () => {
beforeEach(() => {
vm = mountComponent(Component, milestoneMockData);
});
afterEach(() => {
vm.$destroy();
});
it('contains the proper description', () => {
expect(vm.text).toContain('Promoting this milestone will make it available for all projects inside the group.');
});
it('contains the correct title', () => {
expect(vm.title).toEqual('Promote v1.0 to group milestone?');
});
});
describe('When requesting a milestone promotion', () => {
beforeEach(() => {
vm = mountComponent(Component, {
...milestoneMockData,
});
spyOn(eventHub, '$emit');
});
afterEach(() => {
vm.$destroy();
});
it('redirects when a milestone is promoted', (done) => {
const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(milestoneMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestStarted', milestoneMockData.url);
return Promise.resolve({
request: {
responseURL,
},
});
});
vm.onSubmit()
.then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', { milestoneUrl: milestoneMockData.url, successful: true });
})
.then(done)
.catch(done.fail);
});
it('displays an error if promoting a milestone failed', (done) => {
const dummyError = new Error('promoting milestone failed');
dummyError.response = { status: 500 };
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(milestoneMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestStarted', milestoneMockData.url);
return Promise.reject(dummyError);
});
vm.onSubmit()
.catch((error) => {
expect(error).toBe(dummyError);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', { milestoneUrl: milestoneMockData.url, successful: false });
})
.then(done)
.catch(done.fail);
});
});
});
...@@ -335,21 +335,8 @@ describe API::Internal do ...@@ -335,21 +335,8 @@ describe API::Internal do
end end
context "git push" do context "git push" do
context "gitaly disabled", :disable_gitaly do context 'project as namespace/project' do
it "has the correct payload" do it do
push(key, project)
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gitaly"]).to be_nil
expect(user).not_to have_an_activity_record
end
end
context "gitaly enabled" do
it "has the correct payload" do
push(key, project) push(key, project)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
...@@ -365,17 +352,6 @@ describe API::Internal do ...@@ -365,17 +352,6 @@ describe API::Internal do
expect(user).not_to have_an_activity_record expect(user).not_to have_an_activity_record
end end
end end
context 'project as namespace/project' do
it do
push(key, project)
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
end
end
end end
end end
......
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