Commit ace3bcdd authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into 'ce-3839-ci-cd-only-github-projects-fe'

# Conflicts:
#   locale/gitlab.pot
parents d179f002 22198466
...@@ -413,9 +413,7 @@ end ...@@ -413,9 +413,7 @@ end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
# Explicitly lock grpc as we know 1.9 is bad gem 'grpc', '~> 1.10.0'
# 1.10 is still being tested. See gitlab-org/gitaly#1059
gem 'grpc', '~> 1.8.3'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1' gem 'google-protobuf', '= 3.5.1'
......
...@@ -345,9 +345,9 @@ GEM ...@@ -345,9 +345,9 @@ GEM
google-protobuf (3.5.1) google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1) googleapis-common-protos-types (1.0.1)
google-protobuf (~> 3.0) google-protobuf (~> 3.0)
googleauth (0.5.3) googleauth (0.6.2)
faraday (~> 0.12) faraday (~> 0.12)
jwt (~> 1.4) jwt (>= 1.4, < 3.0)
logging (~> 2.0) logging (~> 2.0)
memoist (~> 0.12) memoist (~> 0.12)
multi_json (~> 1.11) multi_json (~> 1.11)
...@@ -371,7 +371,7 @@ GEM ...@@ -371,7 +371,7 @@ GEM
rake rake
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
grpc (1.8.3) grpc (1.10.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7) googleauth (>= 0.5.1, < 0.7)
...@@ -505,7 +505,7 @@ GEM ...@@ -505,7 +505,7 @@ GEM
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.7.0) minitest (5.7.0)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
multi_json (1.12.2) multi_json (1.13.1)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.0.0) multipart-post (2.0.0)
mustermann (1.0.0) mustermann (1.0.0)
...@@ -648,7 +648,7 @@ GEM ...@@ -648,7 +648,7 @@ GEM
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.5) pry-rails (0.3.5)
pry (>= 0.9.10) pry (>= 0.9.10)
public_suffix (3.0.0) public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (1.6.8) rack (1.6.8)
rack-accept (0.4.5) rack-accept (0.4.5)
...@@ -862,10 +862,10 @@ GEM ...@@ -862,10 +862,10 @@ GEM
sidekiq (>= 4.2.1) sidekiq (>= 4.2.1)
sidekiq-limit_fetch (3.4.0) sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4) sidekiq (>= 4)
signet (0.7.3) signet (0.8.1)
addressable (~> 2.3) addressable (~> 2.3)
faraday (~> 0.9) faraday (~> 0.9)
jwt (~> 1.5) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simple_po_parser (1.1.2) simple_po_parser (1.1.2)
simplecov (0.14.1) simplecov (0.14.1)
...@@ -1078,7 +1078,7 @@ DEPENDENCIES ...@@ -1078,7 +1078,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.8.3) grpc (~> 1.10.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
hashie-forbidden_attributes hashie-forbidden_attributes
......
...@@ -71,7 +71,7 @@ star, smile, etc.). Some good tips about code reviews can be found in our ...@@ -71,7 +71,7 @@ star, smile, etc.). Some good tips about code reviews can be found in our
## Feature freeze on the 7th for the release on the 22nd ## Feature freeze on the 7th for the release on the 22nd
After 7th at 23:59 (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it. After 7th at 23:59 (Pacific Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
Merge requests may still be merged into master during this period, Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch. but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
......
<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);
...@@ -6,8 +6,14 @@ ...@@ -6,8 +6,14 @@
constructor(options) { constructor(options) {
this.options = options || {}; this.options = options || {};
this.options.cursorBlink = options.cursorBlink || true; if (!Object.prototype.hasOwnProperty.call(this.options, 'cursorBlink')) {
this.options.screenKeys = options.screenKeys || true; this.options.cursorBlink = true;
}
if (!Object.prototype.hasOwnProperty.call(this.options, 'screenKeys')) {
this.options.screenKeys = true;
}
this.container = document.querySelector(options.selector); this.container = document.querySelector(options.selector);
this.setSocketUrl(); this.setSocketUrl();
......
<script>
export default {
name: 'MRWidgetMaintainerEdit',
props: {
maintainerEditAllowed: {
type: Boolean,
default: false,
required: false,
},
},
};
</script>
<template>
<section class="mr-info-list mr-maintainer-edit">
<p v-if="maintainerEditAllowed">
{{ s__("mrWidget|Allows edits from maintainers") }}
</p>
</section>
</template>
...@@ -15,6 +15,7 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue'; ...@@ -15,6 +15,7 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue'; export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment'; export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetMaintainerEdit } from './components/mr_widget_maintainer_edit.vue';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
export { default as MergedState } from './components/states/mr_widget_merged.vue'; export { default as MergedState } from './components/states/mr_widget_merged.vue';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
WidgetMergeHelp, WidgetMergeHelp,
WidgetPipeline, WidgetPipeline,
WidgetDeployment, WidgetDeployment,
WidgetMaintainerEdit,
WidgetRelatedLinks, WidgetRelatedLinks,
MergedState, MergedState,
ClosedState, ClosedState,
...@@ -211,6 +212,7 @@ export default { ...@@ -211,6 +212,7 @@ export default {
'mr-widget-merge-help': WidgetMergeHelp, 'mr-widget-merge-help': WidgetMergeHelp,
'mr-widget-pipeline': WidgetPipeline, 'mr-widget-pipeline': WidgetPipeline,
'mr-widget-deployment': WidgetDeployment, 'mr-widget-deployment': WidgetDeployment,
'mr-widget-maintainer-edit': WidgetMaintainerEdit,
'mr-widget-related-links': WidgetRelatedLinks, 'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState, 'mr-widget-merged': MergedState,
'mr-widget-closed': ClosedState, 'mr-widget-closed': ClosedState,
...@@ -251,11 +253,12 @@ export default { ...@@ -251,11 +253,12 @@ export default {
:is="componentName" :is="componentName"
:mr="mr" :mr="mr"
:service="service" /> :service="service" />
<mr-widget-maintainer-edit
:maintainerEditAllowed="mr.maintainerEditAllowed" />
<mr-widget-related-links <mr-widget-related-links
v-if="shouldRenderRelatedLinks" v-if="shouldRenderRelatedLinks"
:state="mr.state" :state="mr.state"
:related-links="mr.relatedLinks" :related-links="mr.relatedLinks" />
/>
</div> </div>
<div <div
class="mr-widget-footer" class="mr-widget-footer"
......
...@@ -76,6 +76,7 @@ export default class MergeRequestStore { ...@@ -76,6 +76,7 @@ export default class MergeRequestStore {
this.canBeMerged = data.can_be_merged || false; this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false; this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing; this.mergeOngoing = data.merge_ongoing;
this.maintainerEditAllowed = data.allow_maintainer_to_push;
// Cherry-pick and Revert actions related // Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
......
...@@ -272,7 +272,7 @@ ...@@ -272,7 +272,7 @@
.divider { .divider {
height: 1px; height: 1px;
margin: 6px 0; margin: #{$grid-size / 2} 0;
padding: 0; padding: 0;
background-color: $dropdown-divider-color; background-color: $dropdown-divider-color;
......
...@@ -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 {
......
...@@ -453,7 +453,8 @@ ...@@ -453,7 +453,8 @@
} }
} }
.mr-links { .mr-links,
.mr-maintainer-edit {
padding-left: $status-icon-size + $status-icon-margin; padding-left: $status-icon-size + $status-icon-margin;
} }
......
...@@ -4,7 +4,7 @@ module CreatesCommit ...@@ -4,7 +4,7 @@ module CreatesCommit
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
if can?(current_user, :push_code, @project) if user_access(@project).can_push_to_branch?(branch_name_or_ref)
@project_to_commit_into = @project @project_to_commit_into = @project
@branch_name ||= @ref @branch_name ||= @ref
else else
...@@ -50,7 +50,7 @@ module CreatesCommit ...@@ -50,7 +50,7 @@ module CreatesCommit
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables
def authorize_edit_tree! def authorize_edit_tree!
return if can_collaborate_with_project? return if can_collaborate_with_project?(project, ref: branch_name_or_ref)
access_denied! access_denied!
end end
...@@ -123,4 +123,8 @@ module CreatesCommit ...@@ -123,4 +123,8 @@ module CreatesCommit
params[:create_merge_request].present? && params[:create_merge_request].present? &&
(different_project? || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables (different_project? || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end end
def branch_name_or_ref
@branch_name || @ref # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end end
...@@ -6,7 +6,7 @@ class Projects::ApplicationController < ApplicationController ...@@ -6,7 +6,7 @@ class Projects::ApplicationController < ApplicationController
before_action :repository before_action :repository
layout 'project' layout 'project'
helper_method :repository, :can_collaborate_with_project? helper_method :repository, :can_collaborate_with_project?, :user_access
private private
...@@ -31,11 +31,12 @@ class Projects::ApplicationController < ApplicationController ...@@ -31,11 +31,12 @@ class Projects::ApplicationController < ApplicationController
@repository ||= project.repository @repository ||= project.repository
end end
def can_collaborate_with_project?(project = nil) def can_collaborate_with_project?(project = nil, ref: nil)
project ||= @project project ||= @project
can?(current_user, :push_code, project) || can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project)) (current_user && current_user.already_forked?(project)) ||
user_access(project).can_push_to_branch?(ref)
end end
def authorize_action!(action) def authorize_action!(action)
...@@ -90,4 +91,9 @@ class Projects::ApplicationController < ApplicationController ...@@ -90,4 +91,9 @@ class Projects::ApplicationController < ApplicationController
def check_issues_available! def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user) return render_404 unless @project.feature_available?(:issues, current_user)
end end
def user_access(project)
@user_access ||= {}
@user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project)
end
end end
...@@ -9,8 +9,12 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -9,8 +9,12 @@ class Projects::BlobController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create] before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
# We need to assign the blob vars before `authorize_edit_tree!` so we can
# validate access to a specific ref.
before_action :assign_blob_vars before_action :assign_blob_vars
before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
before_action :commit, except: [:new, :create] before_action :commit, except: [:new, :create]
before_action :blob, except: [:new, :create] before_action :blob, except: [:new, :create]
before_action :require_branch_head, only: [:edit, :update] before_action :require_branch_head, only: [:edit, :update]
...@@ -46,7 +50,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -46,7 +50,7 @@ class Projects::BlobController < Projects::ApplicationController
end end
def edit def edit
if can_collaborate_with_project? if can_collaborate_with_project?(project, ref: @ref)
blob.load_all_data! blob.load_all_data!
else else
redirect_to action: 'show' redirect_to action: 'show'
......
...@@ -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"
......
...@@ -15,6 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -15,6 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes def merge_request_params_attributes
[ [
:allow_maintainer_to_push,
:assignee_id, :assignee_id,
:description, :description,
:force_remove_source_branch, :force_remove_source_branch,
......
...@@ -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
......
...@@ -125,6 +125,19 @@ module MergeRequestsHelper ...@@ -125,6 +125,19 @@ module MergeRequestsHelper
link_to(url[merge_request.project, merge_request], data: data_attrs, &block) link_to(url[merge_request.project, merge_request], data: data_attrs, &block)
end end
def allow_maintainer_push_unavailable_reason(merge_request)
return if merge_request.can_allow_maintainer_to_push?(current_user)
minimum_visibility = [merge_request.target_project.visibility_level,
merge_request.source_project.visibility_level].min
if minimum_visibility < Gitlab::VisibilityLevel::INTERNAL
_('Not available for private projects')
elsif ProtectedBranch.protected?(merge_request.source_project, merge_request.source_branch)
_('Not available for protected branches')
end
end
def merge_params_ee(merge_request) def merge_params_ee(merge_request)
{} {}
end end
......
...@@ -49,15 +49,13 @@ module TreeHelper ...@@ -49,15 +49,13 @@ module TreeHelper
return false unless on_top_of_branch?(project, ref) return false unless on_top_of_branch?(project, ref)
can_collaborate_with_project?(project) can_collaborate_with_project?(project, ref: ref)
end end
def tree_edit_branch(project = @project, ref = @ref) def tree_edit_branch(project = @project, ref = @ref)
return unless can_edit_tree?(project, ref) return unless can_edit_tree?(project, ref)
project = project.present(current_user: current_user) if user_access(project).can_push_to_branch?(ref)
if project.can_current_user_push_to_branch?(ref)
ref ref
else else
project = tree_edit_project(project) project = tree_edit_project(project)
...@@ -88,7 +86,16 @@ module TreeHelper ...@@ -88,7 +86,16 @@ module TreeHelper
end end
def commit_in_fork_help def commit_in_fork_help
"A new branch will be created in your fork and a new merge request will be started." _("A new branch will be created in your fork and a new merge request will be started.")
end
def commit_in_single_accessible_branch
branch_name = html_escape(selected_branch)
message = _("Your changes can be committed to %{branch_name} because a merge "\
"request is open.") % { branch_name: "<strong>#{branch_name}</strong>" }
message.html_safe
end end
def path_breadcrumbs(max_links = 6) def path_breadcrumbs(max_links = 6)
...@@ -125,4 +132,8 @@ module TreeHelper ...@@ -125,4 +132,8 @@ module TreeHelper
return tree.name return tree.name
end end
end end
def selected_branch
@branch_name || tree_edit_branch
end
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
......
...@@ -865,7 +865,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -865,7 +865,7 @@ class MergeRequest < ActiveRecord::Base
def can_be_merged_by?(user) def can_be_merged_by?(user)
access = ::Gitlab::UserAccess.new(user, project: project) access = ::Gitlab::UserAccess.new(user, project: project)
access.can_push_to_branch?(target_branch) || access.can_merge_to_branch?(target_branch) access.can_update_branch?(target_branch)
end end
def can_be_merged_via_command_line_by?(user) def can_be_merged_via_command_line_by?(user)
...@@ -1087,4 +1087,22 @@ class MergeRequest < ActiveRecord::Base ...@@ -1087,4 +1087,22 @@ class MergeRequest < ActiveRecord::Base
project.merge_requests.merged.where(author_id: author_id).empty? project.merge_requests.merged.where(author_id: author_id).empty?
end end
def allow_maintainer_to_push
maintainer_push_possible? && super
end
alias_method :allow_maintainer_to_push?, :allow_maintainer_to_push
def maintainer_push_possible?
source_project.present? && for_fork? &&
target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
!ProtectedBranch.protected?(source_project, source_branch)
end
def can_allow_maintainer_to_push?(user)
maintainer_push_possible? &&
Ability.allowed?(user, :push_code, source_project)
end
end end
...@@ -150,6 +150,7 @@ class Project < ActiveRecord::Base ...@@ -150,6 +150,7 @@ class Project < ActiveRecord::Base
# Merge Requests for target project should be removed with it # Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id' has_many :merge_requests, foreign_key: 'target_project_id'
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues has_many :issues
has_many :labels, class_name: 'ProjectLabel' has_many :labels, class_name: 'ProjectLabel'
has_many :services has_many :services
...@@ -1799,6 +1800,33 @@ class Project < ActiveRecord::Base ...@@ -1799,6 +1800,33 @@ class Project < ActiveRecord::Base
Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end end
def merge_requests_allowing_push_to_user(user)
return MergeRequest.none unless user
developer_access_exists = user.project_authorizations
.where('access_level >= ? ', Gitlab::Access::DEVELOPER)
.where('project_authorizations.project_id = merge_requests.target_project_id')
.limit(1)
.select(1)
source_of_merge_requests.opened
.where(allow_maintainer_to_push: true)
.where('EXISTS (?)', developer_access_exists)
end
def branch_allows_maintainer_push?(user, branch_name)
return false unless user
cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push"
memoized_results = strong_memoize(:branch_allows_maintainer_push) do
Hash.new do |result, cache_key|
result[cache_key] = fetch_branch_allows_maintainer_push?(user, branch_name)
end
end
memoized_results[cache_key]
end
private private
def storage def storage
...@@ -1921,4 +1949,22 @@ class Project < ActiveRecord::Base ...@@ -1921,4 +1949,22 @@ class Project < ActiveRecord::Base
raise ex raise ex
end end
def fetch_branch_allows_maintainer_push?(user, branch_name)
check_access = -> do
merge_request = source_of_merge_requests.opened
.where(allow_maintainer_to_push: true)
.find_by(source_branch: branch_name)
merge_request&.can_be_merged_by?(user)
end
if RequestStore.active?
RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_maintainer_push") do
check_access.call
end
else
check_access.call
end
end
end end
...@@ -651,14 +651,15 @@ class Repository ...@@ -651,14 +651,15 @@ class Repository
end end
def last_commit_for_path(sha, path) def last_commit_for_path(sha, path)
commit_by(oid: last_commit_id_for_path(sha, path)) commit = raw_repository.last_commit_for_path(sha, path)
::Commit.new(commit, @project) if commit
end end
def last_commit_id_for_path(sha, path) def last_commit_id_for_path(sha, path)
key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}"
cache.fetch(key) do cache.fetch(key) do
raw_repository.last_commit_id_for_path(sha, path) last_commit_for_path(sha, path)&.id
end end
end end
......
...@@ -61,6 +61,11 @@ class ProjectPolicy < BasePolicy ...@@ -61,6 +61,11 @@ class ProjectPolicy < BasePolicy
desc "Project has request access enabled" desc "Project has request access enabled"
condition(:request_access_enabled, scope: :subject) { project.request_access_enabled } condition(:request_access_enabled, scope: :subject) { project.request_access_enabled }
desc "Has merge requests allowing pushes to user"
condition(:has_merge_requests_allowing_pushes, scope: :subject) do
project.merge_requests_allowing_push_to_user(user).any?
end
features = %w[ features = %w[
merge_requests merge_requests
issues issues
...@@ -291,6 +296,15 @@ class ProjectPolicy < BasePolicy ...@@ -291,6 +296,15 @@ class ProjectPolicy < BasePolicy
prevent :read_issue prevent :read_issue
end end
# These rules are included to allow maintainers of projects to push to certain
# to run pipelines for the branches they have access to.
rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do
enable :create_build
enable :update_build
enable :create_pipeline
enable :update_pipeline
end
private private
def team_member? def team_member?
......
...@@ -78,7 +78,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -78,7 +78,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
def rebase_path def rebase_path
if !rebase_in_progress? && should_be_rebased? && user_can_push_to_source_branch? if !rebase_in_progress? && should_be_rebased? && can_push_to_source_branch?
rebase_project_merge_request_path(project, merge_request) rebase_project_merge_request_path(project, merge_request)
end end
end end
...@@ -160,7 +160,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -160,7 +160,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
def can_push_to_source_branch? def can_push_to_source_branch?
source_branch_exists? && user_can_push_to_source_branch? return false unless source_branch_exists?
!!::Gitlab::UserAccess
.new(current_user, project: source_project)
.can_push_to_branch?(source_branch)
end end
private private
...@@ -191,17 +195,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -191,17 +195,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end.sort.to_sentence end.sort.to_sentence
end end
def user_can_push_to_source_branch?
return false unless source_branch_exists?
::Gitlab::UserAccess
.new(current_user, project: source_project)
.can_push_to_branch?(source_branch)
end
def user_can_collaborate_with_project? def user_can_collaborate_with_project?
can?(current_user, :push_code, project) || can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project)) (current_user && current_user.already_forked?(project)) ||
can_push_to_source_branch?
end end
def user_can_fork_project? def user_can_fork_project?
......
...@@ -11,6 +11,7 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -11,6 +11,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :source_project_id expose :source_project_id
expose :target_branch expose :target_branch
expose :target_project_id expose :target_project_id
expose :allow_maintainer_to_push
expose :should_be_rebased?, as: :should_be_rebased expose :should_be_rebased?, as: :should_be_rebased
expose :ff_only_enabled do |merge_request| expose :ff_only_enabled do |merge_request|
...@@ -29,6 +30,7 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -29,6 +30,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :can_push_to_source_branch do |merge_request| expose :can_push_to_source_branch do |merge_request|
presenter(merge_request).can_push_to_source_branch? presenter(merge_request).can_push_to_source_branch?
end end
expose :rebase_path do |merge_request| expose :rebase_path do |merge_request|
presenter(merge_request).rebase_path presenter(merge_request).rebase_path
end end
...@@ -136,8 +138,8 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -136,8 +138,8 @@ class MergeRequestWidgetEntity < IssuableEntity
end end
expose :new_blob_path do |merge_request| expose :new_blob_path do |merge_request|
if can?(current_user, :push_code, merge_request.project) if presenter(merge_request).can_push_to_source_branch?
project_new_blob_path(merge_request.project, merge_request.source_branch) project_new_blob_path(merge_request.source_project, merge_request.source_branch)
end end
end end
......
...@@ -81,7 +81,7 @@ module Ci ...@@ -81,7 +81,7 @@ module Ci
end end
def related_merge_requests def related_merge_requests
MergeRequest.opened.where(source_project: pipeline.project, source_branch: pipeline.ref) pipeline.project.source_of_merge_requests.opened.where(source_branch: pipeline.ref)
end end
end end
end end
...@@ -35,6 +35,14 @@ module MergeRequests ...@@ -35,6 +35,14 @@ module MergeRequests
end end
end end
def filter_params(merge_request)
super
unless merge_request.can_allow_maintainer_to_push?(current_user)
params.delete(:allow_maintainer_to_push)
end
end
def merge_request_metrics_service(merge_request) def merge_request_metrics_service(merge_request)
MergeRequestMetricsService.new(merge_request.metrics) MergeRequestMetricsService.new(merge_request.metrics)
end end
......
...@@ -6,6 +6,7 @@ module MergeRequests ...@@ -6,6 +6,7 @@ module MergeRequests
@params_issue_iid = params.delete(:issue_iid) @params_issue_iid = params.delete(:issue_iid)
self.merge_request = MergeRequest.new(params) self.merge_request = MergeRequest.new(params)
merge_request.author = current_user
merge_request.compare_commits = [] merge_request.compare_commits = []
merge_request.source_project = find_source_project merge_request.source_project = find_source_project
merge_request.target_project = find_target_project merge_request.target_project = find_target_project
......
...@@ -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
......
...@@ -3,6 +3,4 @@ ...@@ -3,6 +3,4 @@
= link_to 'Cancel', cancel_path, = link_to 'Cancel', cancel_path,
class: 'btn btn-cancel', data: {confirm: leave_edit_message} class: 'btn btn-cancel', data: {confirm: leave_edit_message}
- unless can?(current_user, :push_code, @project) = render 'shared/projects/edit_information'
.inline.prepend-left-10
= commit_in_fork_help
...@@ -17,6 +17,4 @@ ...@@ -17,6 +17,4 @@
= submit_tag _("Create directory"), class: 'btn btn-create' = submit_tag _("Create directory"), class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project) = render 'shared/projects/edit_information'
.inline.prepend-left-10
= commit_in_fork_help
...@@ -24,6 +24,4 @@ ...@@ -24,6 +24,4 @@
= button_title = button_title
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project) = render 'shared/projects/edit_information'
.inline.prepend-left-10
= commit_in_fork_help
...@@ -35,6 +35,4 @@ ...@@ -35,6 +35,4 @@
= submit_tag label, class: 'btn btn-create' = submit_tag label, class: 'btn btn-create'
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project) = render 'shared/projects/edit_information'
.inline.prepend-left-10
= commit_in_fork_help
...@@ -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
......
- project = @project.present(current_user: current_user)
- branch_name = selected_branch
= render 'shared/commit_message_container', placeholder: placeholder = render 'shared/commit_message_container', placeholder: placeholder
- if @project.empty_repo? - if @project.empty_repo?
...@@ -7,12 +10,14 @@ ...@@ -7,12 +10,14 @@
.form-group.branch .form-group.branch
= label_tag 'branch_name', _('Target Branch'), class: 'control-label' = label_tag 'branch_name', _('Target Branch'), class: 'control-label'
.col-sm-10 .col-sm-10
= text_field_tag 'branch_name', @branch_name || tree_edit_branch, required: true, class: "form-control js-branch-name ref-name" = text_field_tag 'branch_name', branch_name, required: true, class: "form-control js-branch-name ref-name"
.js-create-merge-request-container .js-create-merge-request-container
= render 'shared/new_merge_request_checkbox' = render 'shared/new_merge_request_checkbox'
- elsif project.can_current_user_push_to_branch?(branch_name)
= hidden_field_tag 'branch_name', branch_name
- else - else
= hidden_field_tag 'branch_name', @branch_name || tree_edit_branch = hidden_field_tag 'branch_name', branch_name
= hidden_field_tag 'create_merge_request', 1 = hidden_field_tag 'create_merge_request', 1
= hidden_field_tag 'original_branch', @ref, class: 'js-original-branch' = hidden_field_tag 'original_branch', @ref, class: 'js-original-branch'
...@@ -33,6 +33,8 @@ ...@@ -33,6 +33,8 @@
= render 'shared/issuable/form/merge_params', issuable: issuable = render 'shared/issuable/form/merge_params', issuable: issuable
= render 'shared/issuable/form/contribution', issuable: issuable, form: form
- if @merge_request_to_resolve_discussions_of - if @merge_request_to_resolve_discussions_of
.form-group .form-group
.col-sm-10.col-sm-offset-2 .col-sm-10.col-sm-offset-2
......
- issuable = local_assigns.fetch(:issuable)
- form = local_assigns.fetch(:form)
- return unless issuable.is_a?(MergeRequest)
- return unless issuable.for_fork?
- return unless can?(current_user, :push_code, issuable.source_project)
%hr
.form-group
.control-label
= _('Contribution')
.col-sm-10
.checkbox
= form.label :allow_maintainer_to_push do
= form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user)
= _('Allow edits from maintainers')
= link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access')
.help-block
= allow_maintainer_push_unavailable_reason(issuable)
...@@ -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"
......
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
- if @project.branch_allows_maintainer_push?(current_user, selected_branch)
= commit_in_single_accessible_branch
- else
= commit_in_fork_help
...@@ -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: Set margins around dropdown dividers to 4px
merge_request: 17517
author:
type: fixed
---
title: Allow maintainers to push to forks of their projects when a merge request is open
merge_request: 17395
author:
type: added
---
title: Added new design for promotion modals
merge_request: 17197
author:
type: other
---
title: Upgrade GitLab Workhorse to 4.0.0
merge_request:
author:
type: added
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddAllowMaintainerToPushToMergeRequests < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column :merge_requests, :allow_maintainer_to_push, :boolean
end
def down
remove_column :merge_requests, :allow_maintainer_to_push
end
end
...@@ -1145,6 +1145,7 @@ ActiveRecord::Schema.define(version: 20180307012445) do ...@@ -1145,6 +1145,7 @@ ActiveRecord::Schema.define(version: 20180307012445) do
t.boolean "discussion_locked" t.boolean "discussion_locked"
t.integer "latest_merge_request_diff_id" t.integer "latest_merge_request_diff_id"
t.string "rebase_commit_sha" t.string "rebase_commit_sha"
t.boolean "allow_maintainer_to_push"
end end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
......
...@@ -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
......
...@@ -529,18 +529,19 @@ Creates a new merge request. ...@@ -529,18 +529,19 @@ Creates a new merge request.
POST /projects/:id/merge_requests POST /projects/:id/merge_requests
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `source_branch` | string | yes | The source branch | | `source_branch` | string | yes | The source branch |
| `target_branch` | string | yes | The target branch | | `target_branch` | string | yes | The target branch |
| `title` | string | yes | Title of MR | | `title` | string | yes | Title of MR |
| `assignee_id` | integer | no | Assignee user ID | | `assignee_id` | integer | no | Assignee user ID |
| `description` | string | no | Description of MR | | `description` | string | no | Description of MR |
| `target_project_id` | integer | no | The target project (numeric id) | | `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list | | `labels` | string | no | Labels for MR as a comma-separated list |
| `milestone_id` | integer | no | The ID of a milestone | | `milestone_id` | integer | no | The ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch |
```json ```json
{ {
...@@ -548,7 +549,7 @@ POST /projects/:id/merge_requests ...@@ -548,7 +549,7 @@ POST /projects/:id/merge_requests
"iid": 1, "iid": 1,
"target_branch": "master", "target_branch": "master",
"source_branch": "test1", "source_branch": "test1",
"project_id": 3, "project_id": 4,
"title": "test1", "title": "test1",
"state": "opened", "state": "opened",
"upvotes": 0, "upvotes": 0,
...@@ -569,7 +570,7 @@ POST /projects/:id/merge_requests ...@@ -569,7 +570,7 @@ POST /projects/:id/merge_requests
"state": "active", "state": "active",
"created_at": "2012-04-29T08:46:00Z" "created_at": "2012-04-29T08:46:00Z"
}, },
"source_project_id": 4, "source_project_id": 3,
"target_project_id": 4, "target_project_id": 4,
"labels": [ ], "labels": [ ],
"description": "fixed login page css paddings", "description": "fixed login page css paddings",
...@@ -596,6 +597,7 @@ POST /projects/:id/merge_requests ...@@ -596,6 +597,7 @@ POST /projects/:id/merge_requests
"force_remove_source_branch": false, "force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1", "web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false, "discussion_locked": false,
"allow_maintainer_to_push": false,
"time_stats": { "time_stats": {
"time_estimate": 0, "time_estimate": 0,
"total_time_spent": 0, "total_time_spent": 0,
...@@ -613,19 +615,20 @@ Updates an existing merge request. You can change the target branch, title, or e ...@@ -613,19 +615,20 @@ Updates an existing merge request. You can change the target branch, title, or e
PUT /projects/:id/merge_requests/:merge_request_iid PUT /projects/:id/merge_requests/:merge_request_iid
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The ID of a merge request | | `merge_request_iid` | integer | yes | The ID of a merge request |
| `target_branch` | string | no | The target branch | | `target_branch` | string | no | The target branch |
| `title` | string | no | Title of MR | | `title` | string | no | Title of MR |
| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. | | `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| | `milestone_id` | integer | no | The ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. | | `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `description` | string | no | Description of MR | | `description` | string | no | Description of MR |
| `state_event` | string | no | New state (close/reopen) | | `state_event` | string | no | New state (close/reopen) |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. | | `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. |
| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch |
Must include at least one non-required attribute from above. Must include at least one non-required attribute from above.
...@@ -634,7 +637,7 @@ Must include at least one non-required attribute from above. ...@@ -634,7 +637,7 @@ Must include at least one non-required attribute from above.
"id": 1, "id": 1,
"iid": 1, "iid": 1,
"target_branch": "master", "target_branch": "master",
"project_id": 3, "project_id": 4,
"title": "test1", "title": "test1",
"state": "opened", "state": "opened",
"upvotes": 0, "upvotes": 0,
...@@ -655,7 +658,7 @@ Must include at least one non-required attribute from above. ...@@ -655,7 +658,7 @@ Must include at least one non-required attribute from above.
"state": "active", "state": "active",
"created_at": "2012-04-29T08:46:00Z" "created_at": "2012-04-29T08:46:00Z"
}, },
"source_project_id": 4, "source_project_id": 3,
"target_project_id": 4, "target_project_id": 4,
"labels": [ ], "labels": [ ],
"description": "description1", "description": "description1",
...@@ -682,6 +685,7 @@ Must include at least one non-required attribute from above. ...@@ -682,6 +685,7 @@ Must include at least one non-required attribute from above.
"force_remove_source_branch": false, "force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1", "web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false, "discussion_locked": false,
"allow_maintainer_to_push": false,
"time_stats": { "time_stats": {
"time_estimate": 0, "time_estimate": 0,
"total_time_spent": 0, "total_time_spent": 0,
......
# Dependencies
> TODO: Add Dependencies
\ No newline at end of file
# Development
## [Design patterns](design_patterns.md)
Examples of proven design patterns used in our codebase.
## [Components](components.md)
Documentation on existing components and how to best create a new component.
## [Accessiblity](accessibility.md)
Learn how to implement an accessible frontend.
## [Network requests](network_requests.md)
Learn how to handle network requests in our codebase.
## [Security](security.md)
Learn how to ensure that our frontend is secure.
## [Performance](performance.md)
Learn how to keep our frontend performant.
## [Testing](testing.md)
Learn how to keep our frontend tested.
# Frontend Development Guidelines
This guide contains all the information to successfully contribute to GitLab's frontend.
This is a living document, and we welcome contributions, feedback and suggestions.
## [Principles](principles.md)
Ensure that your frontend contribution starts off in the right direction.
## [Initiatives](initiatives.md)
High level overview of where we are going from a frontend perspective.
## [Development](development/index.md)
Guidance on topics related to development.
## [Dependencies](dependencies.md)
Learn about all the dependencies that make up our frontend, including some of our own custom built libraries.
## [Style](style/index.md)
Style guides to keep our code consistent.
## [Tips](tips.md)
Tips from our frontend team to develop more efficiently and effectively.
# Initiatives
> TODO: Add Initiatives
# Principles
> TODO: Add principles
# HTML style guide
> TODO: Add content
# Style
## [HTML style guide](html.md)
## [SCSS style guide](scss.md)
## [JavaScript style guide](javascript.md)
## [Vue style guide](vue.md)
# JavaScript style guide
> TODO: Add content
# SCSS style guide
> TODO: Add content
# Vue style guide
> TODO: Add content
...@@ -28,6 +28,7 @@ With GitLab merge requests, you can: ...@@ -28,6 +28,7 @@ With GitLab merge requests, you can:
- Enable [fast-forward merge requests](#fast-forward-merge-requests) - Enable [fast-forward merge requests](#fast-forward-merge-requests)
- Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch - Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch
- [Create new merge requests by email](#create-new-merge-requests-by-email) - [Create new merge requests by email](#create-new-merge-requests-by-email)
- Allow maintainers of the target project to push directly to the fork by [allowing edits from maintainers](maintainer_access.md)
With **[GitLab Enterprise Edition][ee]**, you can also: With **[GitLab Enterprise Edition][ee]**, you can also:
......
# Allow maintainer pushes for merge requests accross forks
This feature is available for merge requests across forked projects that are
publicly accessible. It makes it easier for maintainers of projects to collaborate
on merge requests across forks.
When enabling this feature for a merge request, you give can give members with push access to the target project rights to edit files on the source branch of the merge request.
The feature can only be enabled by users who already have push access to the source project. And only lasts while the merge request is open.
Enable this functionality while creating a merge request:
![Enable maintainer edits](./img/allow_maintainer_push.png)
...@@ -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
# ---------------------------------------- # ----------------------------------------
......
...@@ -547,6 +547,7 @@ module API ...@@ -547,6 +547,7 @@ module API
expose :discussion_locked expose :discussion_locked
expose :should_remove_source_branch?, as: :should_remove_source_branch expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch expose :force_remove_source_branch?, as: :force_remove_source_branch
expose :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? }
expose :web_url do |merge_request, options| expose :web_url do |merge_request, options|
Gitlab::UrlBuilder.build(merge_request) Gitlab::UrlBuilder.build(merge_request)
......
...@@ -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),
......
...@@ -144,6 +144,7 @@ module API ...@@ -144,6 +144,7 @@ module API
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: String, desc: 'Comma-separated list of label names' optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
optional :allow_maintainer_to_push, type: Boolean, desc: 'Whether a maintainer of the target project can push to the source project'
use :optional_params_ee use :optional_params_ee
end end
......
...@@ -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
......
...@@ -47,7 +47,7 @@ module Gitlab ...@@ -47,7 +47,7 @@ module Gitlab
protected protected
def push_checks def push_checks
if user_access.cannot_do_action?(:push_code) unless can_push?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code] raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
end end
end end
...@@ -183,6 +183,11 @@ module Gitlab ...@@ -183,6 +183,11 @@ module Gitlab
def commits def commits
@commits ||= project.repository.new_commits(newrev) @commits ||= project.repository.new_commits(newrev)
end end
def can_push?
user_access.can_do_action?(:push_code) ||
user_access.can_push_to_branch?(branch_name)
end
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),
......
...@@ -1443,12 +1443,12 @@ module Gitlab ...@@ -1443,12 +1443,12 @@ module Gitlab
end end
end end
def last_commit_id_for_path(sha, path) def last_commit_for_path(sha, path)
gitaly_migrate(:last_commit_for_path) do |is_enabled| gitaly_migrate(:last_commit_for_path) do |is_enabled|
if is_enabled if is_enabled
last_commit_for_path_by_gitaly(sha, path).id last_commit_for_path_by_gitaly(sha, path)
else else
last_commit_id_for_path_by_shelling_out(sha, path) last_commit_for_path_by_rugged(sha, path)
end end
end end
end end
...@@ -1896,7 +1896,7 @@ module Gitlab ...@@ -1896,7 +1896,7 @@ module Gitlab
end end
def last_commit_for_path_by_rugged(sha, path) def last_commit_for_path_by_rugged(sha, path)
sha = last_commit_id_for_path(sha, path) sha = last_commit_id_for_path_by_shelling_out(sha, path)
commit(sha) commit(sha)
end end
......
...@@ -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
......
...@@ -68,8 +68,10 @@ module Gitlab ...@@ -68,8 +68,10 @@ module Gitlab
return true if project.user_can_push_to_empty_repo?(user) return true if project.user_can_push_to_empty_repo?(user)
protected_branch_accessible_to?(ref, action: :push) protected_branch_accessible_to?(ref, action: :push)
elsif user.can?(:push_code, project)
true
else else
user.can?(:push_code, project) project.branch_allows_maintainer_push?(user, ref)
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
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-05 17:10-0600\n" "POT-Creation-Date: 2018-03-06 17:36+0100\n"
"PO-Revision-Date: 2018-03-05 17:10-0600\n" "PO-Revision-Date: 2018-03-06 17:36+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -105,6 +105,9 @@ msgstr "" ...@@ -105,6 +105,9 @@ msgstr ""
msgid "A collection of graphs regarding Continuous Integration" msgid "A collection of graphs regarding Continuous Integration"
msgstr "" msgstr ""
msgid "A new branch will be created in your fork and a new merge request will be started."
msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}." msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr "" msgstr ""
...@@ -210,6 +213,9 @@ msgstr "" ...@@ -210,6 +213,9 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings." msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr "" msgstr ""
msgid "Allow edits from maintainers"
msgstr ""
msgid "Allows you to add and manage Kubernetes clusters." msgid "Allows you to add and manage Kubernetes clusters."
msgstr "" msgstr ""
...@@ -410,6 +416,15 @@ msgstr "" ...@@ -410,6 +416,15 @@ msgstr ""
msgid "Branches" msgid "Branches"
msgstr "" msgstr ""
msgid "Branches|Active"
msgstr ""
msgid "Branches|Active branches"
msgstr ""
msgid "Branches|All"
msgstr ""
msgid "Branches|Cant find HEAD commit for this branch" msgid "Branches|Cant find HEAD commit for this branch"
msgstr "" msgstr ""
...@@ -455,12 +470,39 @@ msgstr "" ...@@ -455,12 +470,39 @@ msgstr ""
msgid "Branches|Only a project master or owner can delete a protected branch" msgid "Branches|Only a project master or owner can delete a protected branch"
msgstr "" msgstr ""
msgid "Branches|Protected branches can be managed in %{project_settings_link}" msgid "Branches|Overview"
msgstr ""
msgid "Branches|Protected branches can be managed in %{project_settings_link}."
msgstr ""
msgid "Branches|Show active branches"
msgstr ""
msgid "Branches|Show all branches"
msgstr ""
msgid "Branches|Show more active branches"
msgstr ""
msgid "Branches|Show more stale branches"
msgstr ""
msgid "Branches|Show overview of the branches"
msgstr ""
msgid "Branches|Show stale branches"
msgstr "" msgstr ""
msgid "Branches|Sort by" msgid "Branches|Sort by"
msgstr "" msgstr ""
msgid "Branches|Stale"
msgstr ""
msgid "Branches|Stale branches"
msgstr ""
msgid "Branches|The default branch cannot be deleted" msgid "Branches|The default branch cannot be deleted"
msgstr "" msgstr ""
...@@ -1086,6 +1128,9 @@ msgstr "" ...@@ -1086,6 +1128,9 @@ msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images." msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
msgstr "" msgstr ""
msgid "Contribution"
msgstr ""
msgid "Contribution guide" msgid "Contribution guide"
msgstr "" msgstr ""
...@@ -1436,6 +1481,9 @@ msgstr "" ...@@ -1436,6 +1481,9 @@ msgstr ""
msgid "Find file" msgid "Find file"
msgstr "" msgstr ""
msgid "Finished"
msgstr ""
msgid "FirstPushedBy|First" msgid "FirstPushedBy|First"
msgstr "" msgstr ""
...@@ -1969,6 +2017,12 @@ msgstr "" ...@@ -1969,6 +2017,12 @@ msgstr ""
msgid "Not available" msgid "Not available"
msgstr "" msgstr ""
msgid "Not available for private projects"
msgstr ""
msgid "Not available for protected branches"
msgstr ""
msgid "Not confidential" msgid "Not confidential"
msgstr "" msgstr ""
...@@ -2101,6 +2155,9 @@ msgstr "" ...@@ -2101,6 +2155,9 @@ msgstr ""
msgid "Password" msgid "Password"
msgstr "" msgstr ""
msgid "Pending"
msgstr ""
msgid "Personal Access Token" msgid "Personal Access Token"
msgstr "" msgstr ""
...@@ -2182,9 +2239,30 @@ msgstr "" ...@@ -2182,9 +2239,30 @@ msgstr ""
msgid "Pipelines|Build with confidence" msgid "Pipelines|Build with confidence"
msgstr "" msgstr ""
msgid "Pipelines|CI Lint"
msgstr ""
msgid "Pipelines|Clear Runner Caches"
msgstr ""
msgid "Pipelines|Get started with Pipelines" msgid "Pipelines|Get started with Pipelines"
msgstr "" msgstr ""
msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Run Pipeline"
msgstr ""
msgid "Pipelines|There are currently no %{scope} pipelines."
msgstr ""
msgid "Pipelines|There are currently no pipelines."
msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
msgid "Pipeline|Retry pipeline" msgid "Pipeline|Retry pipeline"
msgstr "" msgstr ""
...@@ -2437,6 +2515,9 @@ msgstr "" ...@@ -2437,6 +2515,9 @@ msgstr ""
msgid "Public - The project can be accessed without any authentication." msgid "Public - The project can be accessed without any authentication."
msgstr "" msgstr ""
msgid "Push access to this project is necessary in order to enable this option"
msgstr ""
msgid "Push events" msgid "Push events"
msgstr "" msgstr ""
...@@ -2523,6 +2604,9 @@ msgstr "" ...@@ -2523,6 +2604,9 @@ msgstr ""
msgid "Revert this merge request" msgid "Revert this merge request"
msgstr "" msgstr ""
msgid "Running"
msgstr ""
msgid "SSH Keys" msgid "SSH Keys"
msgstr "" msgstr ""
...@@ -3518,6 +3602,9 @@ msgstr "" ...@@ -3518,6 +3602,9 @@ msgstr ""
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure" msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
msgstr "" msgstr ""
msgid "Your changes can be committed to %{branch_name} because a merge request is open."
msgstr ""
msgid "Your comment will not be visible to the public." msgid "Your comment will not be visible to the public."
msgstr "" msgstr ""
...@@ -3570,6 +3657,9 @@ msgstr[1] "" ...@@ -3570,6 +3657,9 @@ msgstr[1] ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch" msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr "" msgstr ""
msgid "mrWidget|Allows edits from maintainers"
msgstr ""
msgid "mrWidget|Cancel automatic merge" msgid "mrWidget|Cancel automatic merge"
msgstr "" msgstr ""
......
...@@ -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
......
require 'spec_helper'
describe 'a maintainer edits files on a source-branch of an MR from a fork', :js do
include ProjectForksHelper
let(:user) { create(:user, username: 'the-maintainer') }
let(:target_project) { create(:project, :public, :repository) }
let(:author) { create(:user, username: 'mr-authoring-machine') }
let(:source_project) { fork_project(target_project, author, repository: true) }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
target_project: target_project,
source_branch: 'fix',
target_branch: 'master',
author: author,
allow_maintainer_to_push: true)
end
before do
target_project.add_master(user)
sign_in(user)
visit project_merge_request_path(target_project, merge_request)
click_link 'Changes'
wait_for_requests
first('.js-file-title').click_link 'Edit'
wait_for_requests
end
it 'mentions commits will go to the source branch' do
expect(page).to have_content('Your changes can be committed to fix because a merge request is open.')
end
it 'allows committing to the source branch' do
find('.ace_text-input', visible: false).send_keys('Updated the readme')
click_button 'Commit changes'
wait_for_requests
expect(page).to have_content('Your changes have been successfully committed')
expect(page).to have_content('Updated the readme')
end
end
require 'spec_helper'
describe 'create a merge request that allows maintainers to push', :js do
include ProjectForksHelper
let(:user) { create(:user) }
let(:target_project) { create(:project, :public, :repository) }
let(:source_project) { fork_project(target_project, user, repository: true, namespace: user.namespace) }
def visit_new_merge_request
visit project_new_merge_request_path(
source_project,
merge_request: {
source_project_id: source_project.id,
target_project_id: target_project.id,
source_branch: 'fix',
target_branch: 'master'
})
end
before do
sign_in(user)
end
it 'allows setting maintainer push possible' do
visit_new_merge_request
check 'Allow edits from maintainers'
click_button 'Submit merge request'
wait_for_requests
expect(page).to have_content('Allows edits from maintainers')
end
it 'shows a message when one of the projects is private' do
source_project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
visit_new_merge_request
expect(page).to have_content('Not available for private projects')
end
it 'shows a message when the source branch is protected' do
create(:protected_branch, project: source_project, name: 'fix')
visit_new_merge_request
expect(page).to have_content('Not available for protected branches')
end
context 'when the merge request is being created within the same project' do
let(:source_project) { target_project }
it 'hides the checkbox if the merge request is being created within the same project' do
target_project.add_developer(user)
visit_new_merge_request
expect(page).not_to have_content('Allows edits from maintainers')
end
end
context 'when a maintainer tries to edit the option' do
let(:maintainer) { create(:user) }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
target_project: target_project,
source_branch: 'fixes')
end
before do
target_project.add_master(maintainer)
sign_in(maintainer)
end
it 'it hides the option from maintainers' do
visit edit_project_merge_request_path(target_project, merge_request)
expect(page).not_to have_content('Allows edits from maintainers')
end
end
end
...@@ -133,13 +133,20 @@ describe 'User creates files' do ...@@ -133,13 +133,20 @@ describe 'User creates files' do
before do before do
project2.add_reporter(user) project2.add_reporter(user)
visit(project2_tree_path_root_ref) visit(project2_tree_path_root_ref)
end
it 'creates and commit new file in forked project', :js do
find('.add-to-tree').click find('.add-to-tree').click
click_link('New file') click_link('New file')
end
it 'shows a message saying the file will be committed in a fork' do
message = "A new branch will be created in your fork and a new merge request will be started."
expect(page).to have_content(message)
end
it 'creates and commit new file in forked project', :js do
expect(page).to have_selector('.file-editor') expect(page).to have_selector('.file-editor')
expect(page).to have_content
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("ace.edit('editor').setValue('*.rbca')")
......
...@@ -12,7 +12,8 @@ ...@@ -12,7 +12,8 @@
"rebase_in_progress": { "type": "boolean" }, "rebase_in_progress": { "type": "boolean" },
"assignee_id": { "type": ["integer", "null"] }, "assignee_id": { "type": ["integer", "null"] },
"subscribed": { "type": ["boolean", "null"] }, "subscribed": { "type": ["boolean", "null"] },
"participants": { "type": "array" } "participants": { "type": "array" },
"allow_maintainer_to_push": { "type": "boolean"}
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
"source_project_id": { "type": "integer" }, "source_project_id": { "type": "integer" },
"target_branch": { "type": "string" }, "target_branch": { "type": "string" },
"target_project_id": { "type": "integer" }, "target_project_id": { "type": "integer" },
"allow_maintainer_to_push": { "type": "boolean"},
"metrics": { "metrics": {
"oneOf": [ "oneOf": [
{ "type": "null" }, { "type": "null" },
......
...@@ -80,7 +80,8 @@ ...@@ -80,7 +80,8 @@
"total_time_spent": { "type": "integer" }, "total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["string", "null"] }, "human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] } "human_total_time_spent": { "type": ["string", "null"] }
} },
"allow_maintainer_to_push": { "type": ["boolean", "null"] }
}, },
"required": [ "required": [
"id", "iid", "project_id", "title", "description", "id", "iid", "project_id", "title", "description",
......
...@@ -62,4 +62,13 @@ describe TreeHelper do ...@@ -62,4 +62,13 @@ describe TreeHelper do
end end
end end
end end
describe '#commit_in_single_accessible_branch' do
it 'escapes HTML from the branch name' do
helper.instance_variable_set(:@branch_name, "<script>alert('escape me!');</script>")
escaped_branch_name = '&lt;script&gt;alert(&#39;escape me!&#39;);&lt;/script&gt;'
expect(helper.commit_in_single_accessible_branch).to include(escaped_branch_name)
end
end
end end
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);
});
});
});
import Vue from 'vue';
import maintainerEditComponent from '~/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetAuthor', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(maintainerEditComponent);
vm = mountComponent(Component, {
maintainerEditAllowed: true,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders the message when maintainers are allowed to edit', () => {
expect(vm.$el.textContent.trim()).toEqual('Allows edits from maintainers');
});
});
...@@ -349,6 +349,7 @@ describe('mrWidgetOptions', () => { ...@@ -349,6 +349,7 @@ describe('mrWidgetOptions', () => {
expect(comps['mr-widget-pipeline-blocked']).toBeDefined(); expect(comps['mr-widget-pipeline-blocked']).toBeDefined();
expect(comps['mr-widget-pipeline-failed']).toBeDefined(); expect(comps['mr-widget-pipeline-failed']).toBeDefined();
expect(comps['mr-widget-merge-when-pipeline-succeeds']).toBeDefined(); expect(comps['mr-widget-merge-when-pipeline-succeeds']).toBeDefined();
expect(comps['mr-widget-maintainer-edit']).toBeDefined();
}); });
}); });
......
...@@ -30,9 +30,10 @@ describe Gitlab::Checks::ChangeAccess do ...@@ -30,9 +30,10 @@ describe Gitlab::Checks::ChangeAccess do
end end
end end
context 'when the user is not allowed to push code' do context 'when the user is not allowed to push to the repo' do
it 'raises an error' do it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false) expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
expect(user_access).to receive(:can_push_to_branch?).with('master').and_return(false)
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.') expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
end end
......
...@@ -278,6 +278,7 @@ project: ...@@ -278,6 +278,7 @@ project:
- custom_attributes - custom_attributes
- lfs_file_locks - lfs_file_locks
- project_badges - project_badges
- source_of_merge_requests
award_emoji: award_emoji:
- awardable - awardable
- user - user
......
...@@ -168,6 +168,7 @@ MergeRequest: ...@@ -168,6 +168,7 @@ MergeRequest:
- last_edited_by_id - last_edited_by_id
- head_pipeline_id - head_pipeline_id
- discussion_locked - discussion_locked
- allow_maintainer_to_push
MergeRequestDiff: MergeRequestDiff:
- id - id
- state - state
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::UserAccess do describe Gitlab::UserAccess do
include ProjectForksHelper
let(:access) { described_class.new(user, project: project) } let(:access) { described_class.new(user, project: project) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -118,6 +120,39 @@ describe Gitlab::UserAccess do ...@@ -118,6 +120,39 @@ describe Gitlab::UserAccess do
end end
end end
describe 'allowing pushes to maintainers of forked projects' do
let(:canonical_project) { create(:project, :public, :repository) }
let(:project) { fork_project(canonical_project, create(:user), repository: true) }
before do
create(
:merge_request,
target_project: canonical_project,
source_project: project,
source_branch: 'awesome-feature',
allow_maintainer_to_push: true
)
end
it 'allows users that have push access to the canonical project to push to the MR branch' do
canonical_project.add_developer(user)
expect(access.can_push_to_branch?('awesome-feature')).to be_truthy
end
it 'does not allow the user to push to other branches' do
canonical_project.add_developer(user)
expect(access.can_push_to_branch?('master')).to be_falsey
end
it 'does not allow the user to push if he does not have push access to the canonical project' do
canonical_project.add_guest(user)
expect(access.can_push_to_branch?('awesome-feature')).to be_falsey
end
end
describe 'merge to protected branch if allowed for developers' do describe 'merge to protected branch if allowed for developers' do
before do before do
@branch = create :protected_branch, :developers_can_merge, project: project @branch = create :protected_branch, :developers_can_merge, project: project
......
...@@ -2084,4 +2084,82 @@ describe MergeRequest do ...@@ -2084,4 +2084,82 @@ describe MergeRequest do
it_behaves_like 'checking whether a rebase is in progress' it_behaves_like 'checking whether a rebase is in progress'
end end
end end
describe '#allow_maintainer_to_push' do
let(:merge_request) do
build(:merge_request, source_branch: 'fixes', allow_maintainer_to_push: true)
end
it 'is false when pushing by a maintainer is not possible' do
expect(merge_request).to receive(:maintainer_push_possible?) { false }
expect(merge_request.allow_maintainer_to_push).to be_falsy
end
it 'is true when pushing by a maintainer is possible' do
expect(merge_request).to receive(:maintainer_push_possible?) { true }
expect(merge_request.allow_maintainer_to_push).to be_truthy
end
end
describe '#maintainer_push_possible?' do
let(:merge_request) do
build(:merge_request, source_branch: 'fixes')
end
before do
allow(ProtectedBranch).to receive(:protected?) { false }
end
it 'does not allow maintainer to push if the source project is the same as the target' do
merge_request.target_project = merge_request.source_project = create(:project, :public)
expect(merge_request.maintainer_push_possible?).to be_falsy
end
it 'allows maintainer to push when both source and target are public' do
merge_request.target_project = build(:project, :public)
merge_request.source_project = build(:project, :public)
expect(merge_request.maintainer_push_possible?).to be_truthy
end
it 'is not available for protected branches' do
merge_request.target_project = build(:project, :public)
merge_request.source_project = build(:project, :public)
expect(ProtectedBranch).to receive(:protected?)
.with(merge_request.source_project, 'fixes')
.and_return(true)
expect(merge_request.maintainer_push_possible?).to be_falsy
end
end
describe '#can_allow_maintainer_to_push?' do
let(:target_project) { create(:project, :public) }
let(:source_project) { fork_project(target_project) }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
source_branch: 'fixes',
target_project: target_project)
end
let(:user) { create(:user) }
before do
allow(merge_request).to receive(:maintainer_push_possible?) { true }
end
it 'is false if the user does not have push access to the source project' do
expect(merge_request.can_allow_maintainer_to_push?(user)).to be_falsy
end
it 'is true when the user has push access to the source project' do
source_project.add_developer(user)
expect(merge_request.can_allow_maintainer_to_push?(user)).to be_truthy
end
end
end end
require 'spec_helper' require 'spec_helper'
describe Project do describe Project do
include ProjectForksHelper
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:group) } it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) } it { is_expected.to belong_to(:namespace) }
...@@ -3378,4 +3380,103 @@ describe Project do ...@@ -3378,4 +3380,103 @@ describe Project do
end end
end end
end end
context 'with cross project merge requests' do
let(:user) { create(:user) }
let(:target_project) { create(:project, :repository) }
let(:project) { fork_project(target_project, nil, repository: true) }
let!(:merge_request) do
create(
:merge_request,
target_project: target_project,
target_branch: 'target-branch',
source_project: project,
source_branch: 'awesome-feature-1',
allow_maintainer_to_push: true
)
end
before do
target_project.add_developer(user)
end
describe '#merge_requests_allowing_push_to_user' do
it 'returns open merge requests for which the user has developer access to the target project' do
expect(project.merge_requests_allowing_push_to_user(user)).to include(merge_request)
end
it 'does not include closed merge requests' do
merge_request.close
expect(project.merge_requests_allowing_push_to_user(user)).to be_empty
end
it 'does not include merge requests for guest users' do
guest = create(:user)
target_project.add_guest(guest)
expect(project.merge_requests_allowing_push_to_user(guest)).to be_empty
end
it 'does not include the merge request for other users' do
other_user = create(:user)
expect(project.merge_requests_allowing_push_to_user(other_user)).to be_empty
end
it 'is empty when no user is passed' do
expect(project.merge_requests_allowing_push_to_user(nil)).to be_empty
end
end
describe '#branch_allows_maintainer_push?' do
it 'allows access if the user can merge the merge request' do
expect(project.branch_allows_maintainer_push?(user, 'awesome-feature-1'))
.to be_truthy
end
it 'does not allow guest users access' do
guest = create(:user)
target_project.add_guest(guest)
expect(project.branch_allows_maintainer_push?(guest, 'awesome-feature-1'))
.to be_falsy
end
it 'does not allow access to branches for which the merge request was closed' do
create(:merge_request, :closed,
target_project: target_project,
target_branch: 'target-branch',
source_project: project,
source_branch: 'rejected-feature-1',
allow_maintainer_to_push: true)
expect(project.branch_allows_maintainer_push?(user, 'rejected-feature-1'))
.to be_falsy
end
it 'does not allow access if the user cannot merge the merge request' do
create(:protected_branch, :masters_can_push, project: target_project, name: 'target-branch')
expect(project.branch_allows_maintainer_push?(user, 'awesome-feature-1'))
.to be_falsy
end
it 'caches the result' do
control = ActiveRecord::QueryRecorder.new { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') }
expect { 3.times { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') } }
.not_to exceed_query_limit(control)
end
context 'when the requeststore is active', :request_store do
it 'only queries per project across instances' do
control = ActiveRecord::QueryRecorder.new { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') }
expect { 2.times { described_class.find(project.id).branch_allows_maintainer_push?(user, 'awesome-feature-1') } }
.not_to exceed_query_limit(control).with_threshold(2)
end
end
end
end
end end
...@@ -308,4 +308,41 @@ describe ProjectPolicy do ...@@ -308,4 +308,41 @@ describe ProjectPolicy do
it_behaves_like 'project policies as master' it_behaves_like 'project policies as master'
it_behaves_like 'project policies as owner' it_behaves_like 'project policies as owner'
it_behaves_like 'project policies as admin' it_behaves_like 'project policies as admin'
context 'when a public project has merge requests allowing access' do
include ProjectForksHelper
let(:user) { create(:user) }
let(:target_project) { create(:project, :public) }
let(:project) { fork_project(target_project) }
let!(:merge_request) do
create(
:merge_request,
target_project: target_project,
source_project: project,
allow_maintainer_to_push: true
)
end
let(:maintainer_abilities) do
%w(create_build update_build create_pipeline update_pipeline)
end
subject { described_class.new(user, project) }
it 'does not allow pushing code' do
expect_disallowed(*maintainer_abilities)
end
it 'allows pushing if the user is a member with push access to the target project' do
target_project.add_developer(user)
expect_allowed(*maintainer_abilities)
end
it 'dissallows abilities to a maintainer if the merge request was closed' do
target_project.add_developer(user)
merge_request.close!
expect_disallowed(*maintainer_abilities)
end
end
end end
...@@ -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
......
...@@ -616,6 +616,25 @@ describe API::MergeRequests do ...@@ -616,6 +616,25 @@ describe API::MergeRequests do
expect(json_response['changes_count']).to eq('5+') expect(json_response['changes_count']).to eq('5+')
end end
end end
context 'for forked projects' do
let(:user2) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:forked_project) { fork_project(project, user2, repository: true) }
let(:merge_request) do
create(:merge_request,
source_project: forked_project,
target_project: project,
source_branch: 'fixes',
allow_maintainer_to_push: true)
end
it 'includes the `allow_maintainer_to_push` field' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(json_response['allow_maintainer_to_push']).to be_truthy
end
end
end end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do
...@@ -815,6 +834,7 @@ describe API::MergeRequests do ...@@ -815,6 +834,7 @@ describe API::MergeRequests do
context 'forked projects' do context 'forked projects' do
let!(:user2) { create(:user) } let!(:user2) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let!(:forked_project) { fork_project(project, user2, repository: true) } let!(:forked_project) { fork_project(project, user2, repository: true) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
...@@ -872,6 +892,14 @@ describe API::MergeRequests do ...@@ -872,6 +892,14 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(400)
end end
it 'allows setting `allow_maintainer_to_push`' do
post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
author: user2, target_project_id: project.id, allow_maintainer_to_push: true
expect(response).to have_gitlab_http_status(201)
expect(json_response['allow_maintainer_to_push']).to be_truthy
end
context 'when target_branch and target_project_id is specified' do context 'when target_branch and target_project_id is specified' do
let(:params) do let(:params) do
{ title: 'Test merge_request', { title: 'Test merge_request',
......
require 'spec_helper' require 'spec_helper'
describe MergeRequests::UpdateService, :mailer do describe MergeRequests::UpdateService, :mailer do
include ProjectForksHelper
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
...@@ -538,5 +540,40 @@ describe MergeRequests::UpdateService, :mailer do ...@@ -538,5 +540,40 @@ describe MergeRequests::UpdateService, :mailer do
let(:open_issuable) { merge_request } let(:open_issuable) { merge_request }
let(:closed_issuable) { create(:closed_merge_request, source_project: project) } let(:closed_issuable) { create(:closed_merge_request, source_project: project) }
end end
context 'setting `allow_maintainer_to_push`' do
let(:target_project) { create(:project, :public) }
let(:source_project) { fork_project(target_project) }
let(:user) { create(:user) }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
source_branch: 'fixes',
target_project: target_project)
end
before do
allow(ProtectedBranch).to receive(:protected?).with(source_project, 'fixes') { false }
end
it 'does not allow a maintainer of the target project to set `allow_maintainer_to_push`' do
target_project.add_developer(user)
update_merge_request(allow_maintainer_to_push: true, title: 'Updated title')
expect(merge_request.title).to eq('Updated title')
expect(merge_request.allow_maintainer_to_push).to be_falsy
end
it 'is allowed by a user that can push to the source and can update the merge request' do
merge_request.update!(assignee: user)
source_project.add_developer(user)
update_merge_request(allow_maintainer_to_push: true, title: 'Updated title')
expect(merge_request.title).to eq('Updated title')
expect(merge_request.allow_maintainer_to_push).to be_truthy
end
end
end end
end end
...@@ -13,6 +13,7 @@ describe 'projects/tree/show' do ...@@ -13,6 +13,7 @@ describe 'projects/tree/show' do
allow(view).to receive(:can?).and_return(true) allow(view).to receive(:can?).and_return(true)
allow(view).to receive(:can_collaborate_with_project?).and_return(true) allow(view).to receive(:can_collaborate_with_project?).and_return(true)
allow(view).to receive_message_chain('user_access.can_push_to_branch?').and_return(true)
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings) allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
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