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
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
# Explicitly lock grpc as we know 1.9 is bad
# 1.10 is still being tested. See gitlab-org/gitaly#1059
gem 'grpc', '~> 1.8.3'
gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
......
......@@ -345,9 +345,9 @@ GEM
google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1)
google-protobuf (~> 3.0)
googleauth (0.5.3)
googleauth (0.6.2)
faraday (~> 0.12)
jwt (~> 1.4)
jwt (>= 1.4, < 3.0)
logging (~> 2.0)
memoist (~> 0.12)
multi_json (~> 1.11)
......@@ -371,7 +371,7 @@ GEM
rake
grape_logging (1.7.0)
grape
grpc (1.8.3)
grpc (1.10.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
......@@ -505,7 +505,7 @@ GEM
mini_portile2 (2.3.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
multi_json (1.12.2)
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
mustermann (1.0.0)
......@@ -648,7 +648,7 @@ GEM
pry (~> 0.10)
pry-rails (0.3.5)
pry (>= 0.9.10)
public_suffix (3.0.0)
public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.8)
rack-accept (0.4.5)
......@@ -862,10 +862,10 @@ GEM
sidekiq (>= 4.2.1)
sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4)
signet (0.7.3)
signet (0.8.1)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (~> 1.5)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simple_po_parser (1.1.2)
simplecov (0.14.1)
......@@ -1078,7 +1078,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
grpc (~> 1.8.3)
grpc (~> 1.10.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
......
......@@ -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
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,
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 Translate from '~/vue_shared/translate';
import deleteMilestoneModal from './components/delete_milestone_modal.vue';
import eventHub from './event_hub';
import initDeleteMilestoneModal from './delete_milestone_modal_init';
import initPromoteMilestoneModal from './promote_milestone_modal_init';
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');
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,
});
},
});
initDeleteMilestoneModal();
initPromoteMilestoneModal();
};
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 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 @@
constructor(options) {
this.options = options || {};
this.options.cursorBlink = options.cursorBlink || true;
this.options.screenKeys = options.screenKeys || true;
if (!Object.prototype.hasOwnProperty.call(this.options, 'cursorBlink')) {
this.options.cursorBlink = true;
}
if (!Object.prototype.hasOwnProperty.call(this.options, 'screenKeys')) {
this.options.screenKeys = true;
}
this.container = document.querySelector(options.selector);
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';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
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 MergedState } from './components/states/mr_widget_merged.vue';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
......
......@@ -6,6 +6,7 @@ import {
WidgetMergeHelp,
WidgetPipeline,
WidgetDeployment,
WidgetMaintainerEdit,
WidgetRelatedLinks,
MergedState,
ClosedState,
......@@ -211,6 +212,7 @@ export default {
'mr-widget-merge-help': WidgetMergeHelp,
'mr-widget-pipeline': WidgetPipeline,
'mr-widget-deployment': WidgetDeployment,
'mr-widget-maintainer-edit': WidgetMaintainerEdit,
'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState,
'mr-widget-closed': ClosedState,
......@@ -251,11 +253,12 @@ export default {
:is="componentName"
:mr="mr"
:service="service" />
<mr-widget-maintainer-edit
:maintainerEditAllowed="mr.maintainerEditAllowed" />
<mr-widget-related-links
v-if="shouldRenderRelatedLinks"
:state="mr.state"
:related-links="mr.relatedLinks"
/>
:related-links="mr.relatedLinks" />
</div>
<div
class="mr-widget-footer"
......
......@@ -76,6 +76,7 @@ export default class MergeRequestStore {
this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
this.maintainerEditAllowed = data.allow_maintainer_to_push;
// Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
......
......@@ -272,7 +272,7 @@
.divider {
height: 1px;
margin: 6px 0;
margin: #{$grid-size / 2} 0;
padding: 0;
background-color: $dropdown-divider-color;
......
......@@ -2,14 +2,17 @@
background-color: $modal-body-bg;
padding: #{3 * $grid-size} #{2 * $grid-size};
.page-title {
margin-top: 0;
.page-title,
.modal-title {
.color-label {
font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal;
}
}
.page-title {
margin-top: 0;
}
}
.modal-body {
......
......@@ -453,7 +453,8 @@
}
}
.mr-links {
.mr-links,
.mr-maintainer-edit {
padding-left: $status-icon-size + $status-icon-margin;
}
......
......@@ -4,7 +4,7 @@ module CreatesCommit
# rubocop:disable Gitlab/ModuleWithInstanceVariables
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
@branch_name ||= @ref
else
......@@ -50,7 +50,7 @@ module CreatesCommit
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def authorize_edit_tree!
return if can_collaborate_with_project?
return if can_collaborate_with_project?(project, ref: branch_name_or_ref)
access_denied!
end
......@@ -123,4 +123,8 @@ module CreatesCommit
params[:create_merge_request].present? &&
(different_project? || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def branch_name_or_ref
@branch_name || @ref # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
......@@ -6,7 +6,7 @@ class Projects::ApplicationController < ApplicationController
before_action :repository
layout 'project'
helper_method :repository, :can_collaborate_with_project?
helper_method :repository, :can_collaborate_with_project?, :user_access
private
......@@ -31,11 +31,12 @@ class Projects::ApplicationController < ApplicationController
@repository ||= project.repository
end
def can_collaborate_with_project?(project = nil)
def can_collaborate_with_project?(project = nil, ref: nil)
project ||= @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
def authorize_action!(action)
......@@ -90,4 +91,9 @@ class Projects::ApplicationController < ApplicationController
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
def user_access(project)
@user_access ||= {}
@user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project)
end
end
......@@ -9,8 +9,12 @@ class Projects::BlobController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create]
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 :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
before_action :commit, except: [:new, :create]
before_action :blob, except: [:new, :create]
before_action :require_branch_head, only: [:edit, :update]
......@@ -46,7 +50,7 @@ class Projects::BlobController < Projects::ApplicationController
end
def edit
if can_collaborate_with_project?
if can_collaborate_with_project?(project, ref: @ref)
blob.load_all_data!
else
redirect_to action: 'show'
......
......@@ -112,12 +112,14 @@ class Projects::LabelsController < Projects::ApplicationController
begin
return render_404 unless promote_service.execute(@label)
flash[:notice] = "#{@label.title} promoted to group label."
respond_to do |format|
format.html do
redirect_to(project_labels_path(@project),
notice: 'Label was promoted to a Group Label')
redirect_to(project_labels_path(@project), status: 303)
end
format.json do
render json: { url: project_labels_path(@project) }
end
format.js
end
rescue ActiveRecord::RecordInvalid => e
Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label"
......
......@@ -15,6 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes
[
:allow_maintainer_to_push,
:assignee_id,
:description,
:force_remove_source_branch,
......
......@@ -70,9 +70,17 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def promote
promoted_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)
Milestones::PromoteService.new(project, current_user).execute(milestone)
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
redirect_to milestone, alert: error.message
end
......
......@@ -125,6 +125,19 @@ module MergeRequestsHelper
link_to(url[merge_request.project, merge_request], data: data_attrs, &block)
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)
{}
end
......
......@@ -49,15 +49,13 @@ module TreeHelper
return false unless on_top_of_branch?(project, ref)
can_collaborate_with_project?(project)
can_collaborate_with_project?(project, ref: ref)
end
def tree_edit_branch(project = @project, ref = @ref)
return unless can_edit_tree?(project, ref)
project = project.present(current_user: current_user)
if project.can_current_user_push_to_branch?(ref)
if user_access(project).can_push_to_branch?(ref)
ref
else
project = tree_edit_project(project)
......@@ -88,7 +86,16 @@ module TreeHelper
end
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
def path_breadcrumbs(max_links = 6)
......@@ -125,4 +132,8 @@ module TreeHelper
return tree.name
end
end
def selected_branch
@branch_name || tree_edit_branch
end
end
......@@ -117,7 +117,7 @@ class Notify < BaseMailer
if Gitlab::IncomingEmail.enabled? && @sent_notification
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
......
......@@ -865,7 +865,7 @@ class MergeRequest < ActiveRecord::Base
def can_be_merged_by?(user)
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
def can_be_merged_via_command_line_by?(user)
......@@ -1087,4 +1087,22 @@ class MergeRequest < ActiveRecord::Base
project.merge_requests.merged.where(author_id: author_id).empty?
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
......@@ -150,6 +150,7 @@ class Project < ActiveRecord::Base
# Merge Requests for target project should be removed with it
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 :labels, class_name: 'ProjectLabel'
has_many :services
......@@ -1799,6 +1800,33 @@ class Project < ActiveRecord::Base
Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
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
def storage
......@@ -1921,4 +1949,22 @@ class Project < ActiveRecord::Base
raise ex
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
......@@ -651,14 +651,15 @@ class Repository
end
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
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)}"
cache.fetch(key) do
raw_repository.last_commit_id_for_path(sha, path)
last_commit_for_path(sha, path)&.id
end
end
......
......@@ -61,6 +61,11 @@ class ProjectPolicy < BasePolicy
desc "Project has 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[
merge_requests
issues
......@@ -291,6 +296,15 @@ class ProjectPolicy < BasePolicy
prevent :read_issue
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
def team_member?
......
......@@ -78,7 +78,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
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)
end
end
......@@ -160,7 +160,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
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
private
......@@ -191,17 +195,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end.sort.to_sentence
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?
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
def user_can_fork_project?
......
......@@ -11,6 +11,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :source_project_id
expose :target_branch
expose :target_project_id
expose :allow_maintainer_to_push
expose :should_be_rebased?, as: :should_be_rebased
expose :ff_only_enabled do |merge_request|
......@@ -29,6 +30,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :can_push_to_source_branch do |merge_request|
presenter(merge_request).can_push_to_source_branch?
end
expose :rebase_path do |merge_request|
presenter(merge_request).rebase_path
end
......@@ -136,8 +138,8 @@ class MergeRequestWidgetEntity < IssuableEntity
end
expose :new_blob_path do |merge_request|
if can?(current_user, :push_code, merge_request.project)
project_new_blob_path(merge_request.project, merge_request.source_branch)
if presenter(merge_request).can_push_to_source_branch?
project_new_blob_path(merge_request.source_project, merge_request.source_branch)
end
end
......
......@@ -81,7 +81,7 @@ module Ci
end
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
......@@ -35,6 +35,14 @@ module MergeRequests
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)
MergeRequestMetricsService.new(merge_request.metrics)
end
......
......@@ -6,6 +6,7 @@ module MergeRequests
@params_issue_iid = params.delete(:issue_iid)
self.merge_request = MergeRequest.new(params)
merge_request.author = current_user
merge_request.compare_commits = []
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
......
......@@ -85,7 +85,7 @@ module Projects
end
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?
@project.write_repository_config
......
......@@ -3,6 +3,4 @@
= link_to 'Cancel', cancel_path,
class: 'btn btn-cancel', data: {confirm: leave_edit_message}
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
= render 'shared/projects/edit_information'
......@@ -17,6 +17,4 @@
= submit_tag _("Create directory"), class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
= render 'shared/projects/edit_information'
......@@ -24,6 +24,4 @@
= button_title
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
= render 'shared/projects/edit_information'
......@@ -35,6 +35,4 @@
= submit_tag label, class: 'btn btn-create'
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
= render 'shared/projects/edit_information'
......@@ -4,6 +4,7 @@
- can_admin_label = can?(current_user, :admin_label, @project)
- if @labels.exists? || @prioritized_labels.exists?
#promote-label-modal
%div{ class: container_class }
.top-area.adjust
.nav-text
......
......@@ -13,6 +13,7 @@
.milestones
#delete-milestone-modal
#promote-milestone-modal
%ul.content-list
= render @milestones
......
......@@ -27,8 +27,15 @@
Edit
- 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
Promote
%button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
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?
= 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 @@
.pull-right.hidden-xs.hidden-sm
- 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
%span.sr-only Promote to Group
%button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
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')
- 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
......
- project = @project.present(current_user: current_user)
- branch_name = selected_branch
= render 'shared/commit_message_container', placeholder: placeholder
- if @project.empty_repo?
......@@ -7,12 +10,14 @@
.form-group.branch
= label_tag 'branch_name', _('Target Branch'), class: 'control-label'
.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
= render 'shared/new_merge_request_checkbox'
- elsif project.can_current_user_push_to_branch?(branch_name)
= hidden_field_tag 'branch_name', branch_name
- 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 'original_branch', @ref, class: 'js-original-branch'
......@@ -33,6 +33,8 @@
= render 'shared/issuable/form/merge_params', issuable: issuable
= render 'shared/issuable/form/contribution', issuable: issuable, form: form
- if @merge_request_to_resolve_discussions_of
.form-group
.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 @@
\
- 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
Promote
%button.js-promote-project-milestone-button.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
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"
......
- 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
# These are input errors and won't be corrected even if Sidekiq retries
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
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
t.boolean "discussion_locked"
t.integer "latest_merge_request_diff_id"
t.string "rebase_commit_sha"
t.boolean "allow_maintainer_to_push"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
......
......@@ -37,7 +37,7 @@ following locations:
- [Group milestones](group_milestones.md)
- [Namespaces](namespaces.md)
- [Notes](notes.md) (comments)
- [Threaded comments](discussions.md)
- [Discussions](discussions.md) (threaded comments)
- [Notification settings](notification_settings.md)
- [Open source license templates](templates/licenses.md)
- [Pages Domains](pages_domains.md)
......
# Discussions API
Discussions are set of related notes on snippets, issues or epics.
Discussions are set of related notes on snippets or issues.
## Issues
......
......@@ -529,18 +529,19 @@ Creates a new merge request.
POST /projects/:id/merge_requests
```
| 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 |
| `source_branch` | string | yes | The source branch |
| `target_branch` | string | yes | The target branch |
| `title` | string | yes | Title of MR |
| `assignee_id` | integer | no | Assignee user ID |
| `description` | string | no | Description of MR |
| `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list |
| `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 |
| 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 |
| `source_branch` | string | yes | The source branch |
| `target_branch` | string | yes | The target branch |
| `title` | string | yes | Title of MR |
| `assignee_id` | integer | no | Assignee user ID |
| `description` | string | no | Description of MR |
| `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list |
| `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 |
| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch |
```json
{
......@@ -548,7 +549,7 @@ POST /projects/:id/merge_requests
"iid": 1,
"target_branch": "master",
"source_branch": "test1",
"project_id": 3,
"project_id": 4,
"title": "test1",
"state": "opened",
"upvotes": 0,
......@@ -569,7 +570,7 @@ POST /projects/:id/merge_requests
"state": "active",
"created_at": "2012-04-29T08:46:00Z"
},
"source_project_id": 4,
"source_project_id": 3,
"target_project_id": 4,
"labels": [ ],
"description": "fixed login page css paddings",
......@@ -596,6 +597,7 @@ POST /projects/:id/merge_requests
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"allow_maintainer_to_push": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
......@@ -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
```
| 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 |
| `merge_request_iid` | integer | yes | The ID of a merge request |
| `target_branch` | string | no | The target branch |
| `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. |
| `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. |
| `description` | string | no | Description of MR |
| `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 |
| `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. |
| 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 |
| `merge_request_iid` | integer | yes | The ID of a merge request |
| `target_branch` | string | no | The target branch |
| `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. |
| `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. |
| `description` | string | no | Description of MR |
| `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 |
| `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.
......@@ -634,7 +637,7 @@ Must include at least one non-required attribute from above.
"id": 1,
"iid": 1,
"target_branch": "master",
"project_id": 3,
"project_id": 4,
"title": "test1",
"state": "opened",
"upvotes": 0,
......@@ -655,7 +658,7 @@ Must include at least one non-required attribute from above.
"state": "active",
"created_at": "2012-04-29T08:46:00Z"
},
"source_project_id": 4,
"source_project_id": 3,
"target_project_id": 4,
"labels": [ ],
"description": "description1",
......@@ -682,6 +685,7 @@ Must include at least one non-required attribute from above.
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"allow_maintainer_to_push": false,
"time_stats": {
"time_estimate": 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:
- 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
- [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:
......
# 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
step 'I should see project "Shop" activity feed' do
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
step 'I should see project settings' do
......@@ -113,12 +113,12 @@ module SharedProject
step 'I should not see project "Archive"' do
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
step 'I should see project "Archive"' do
project = Project.find_by(name: "Archive")
expect(page).to have_content project.name_with_namespace
expect(page).to have_content project.full_name
end
# ----------------------------------------
......
......@@ -547,6 +547,7 @@ module API
expose :discussion_locked
expose :should_remove_source_branch?, as: :should_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|
Gitlab::UrlBuilder.build(merge_request)
......
......@@ -111,13 +111,6 @@ module API
def gitaly_payload(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,
address: Gitlab::GitalyClient.address(project.repository_storage),
......
......@@ -144,6 +144,7 @@ module API
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 :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
end
......
......@@ -135,7 +135,7 @@ module Gitlab
if label.valid?
@labels[label_params[:title]] = label
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
......
......@@ -47,7 +47,7 @@ module Gitlab
protected
def push_checks
if user_access.cannot_do_action?(:push_code)
unless can_push?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
end
end
......@@ -183,6 +183,11 @@ module Gitlab
def commits
@commits ||= project.repository.new_commits(newrev)
end
def can_push?
user_access.can_do_action?(:push_code) ||
user_access.can_push_to_branch?(branch_name)
end
end
end
end
......@@ -31,7 +31,7 @@ module Gitlab
# TODO: do we still need it?
project_id: project.id,
project_name: project.name_with_namespace,
project_name: project.full_name,
user: {
id: user.try(:id),
......
......@@ -1443,12 +1443,12 @@ module Gitlab
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|
if is_enabled
last_commit_for_path_by_gitaly(sha, path).id
last_commit_for_path_by_gitaly(sha, path)
else
last_commit_id_for_path_by_shelling_out(sha, path)
last_commit_for_path_by_rugged(sha, path)
end
end
end
......@@ -1896,7 +1896,7 @@ module Gitlab
end
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)
end
......
......@@ -38,7 +38,7 @@ module Gitlab
end
def project_link
"[#{project.name_with_namespace}](#{project.web_url})"
"[#{project.full_name}](#{project.web_url})"
end
def author_profile_link
......
......@@ -53,7 +53,7 @@ module Gitlab
end
def pretext
"Issue *#{@resource.to_reference}* from #{project.name_with_namespace}"
"Issue *#{@resource.to_reference}* from #{project.full_name}"
end
end
end
......
......@@ -68,8 +68,10 @@ module Gitlab
return true if project.user_can_push_to_empty_repo?(user)
protected_branch_accessible_to?(ref, action: :push)
elsif user.can?(:push_code, project)
true
else
user.can?(:push_code, project)
project.branch_allows_maintainer_push?(user, ref)
end
end
......
......@@ -10,6 +10,7 @@ module Gitlab
INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze
INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.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
# bytes https://tools.ietf.org/html/rfc4868#section-2.6
......@@ -17,6 +18,8 @@ module Gitlab
class << self
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
repo_path = repository.path_to_repo
params = {
......@@ -31,24 +34,7 @@ module Gitlab
token: Gitlab::GitalyClient.token(project.repository_storage)
}
params[:Repository] = repository.gitaly_repository.to_h
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[:GitalyServer] = server
params
end
......
......@@ -50,7 +50,7 @@ module SystemCheck
if should_sanitize?
"#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... "
else
"#{project.name_with_namespace.color(:yellow)} ... "
"#{project.full_name.color(:yellow)} ... "
end
end
......
......@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-05 17:10-0600\n"
"PO-Revision-Date: 2018-03-05 17:10-0600\n"
"POT-Creation-Date: 2018-03-06 17:36+0100\n"
"PO-Revision-Date: 2018-03-06 17:36+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -105,6 +105,9 @@ msgstr ""
msgid "A collection of graphs regarding Continuous Integration"
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}."
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."
msgstr ""
msgid "Allow edits from maintainers"
msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
......@@ -410,6 +416,15 @@ msgstr ""
msgid "Branches"
msgstr ""
msgid "Branches|Active"
msgstr ""
msgid "Branches|Active branches"
msgstr ""
msgid "Branches|All"
msgstr ""
msgid "Branches|Cant find HEAD commit for this branch"
msgstr ""
......@@ -455,12 +470,39 @@ msgstr ""
msgid "Branches|Only a project master or owner can delete a protected branch"
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 ""
msgid "Branches|Sort by"
msgstr ""
msgid "Branches|Stale"
msgstr ""
msgid "Branches|Stale branches"
msgstr ""
msgid "Branches|The default branch cannot be deleted"
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."
msgstr ""
msgid "Contribution"
msgstr ""
msgid "Contribution guide"
msgstr ""
......@@ -1436,6 +1481,9 @@ msgstr ""
msgid "Find file"
msgstr ""
msgid "Finished"
msgstr ""
msgid "FirstPushedBy|First"
msgstr ""
......@@ -1969,6 +2017,12 @@ msgstr ""
msgid "Not available"
msgstr ""
msgid "Not available for private projects"
msgstr ""
msgid "Not available for protected branches"
msgstr ""
msgid "Not confidential"
msgstr ""
......@@ -2101,6 +2155,9 @@ msgstr ""
msgid "Password"
msgstr ""
msgid "Pending"
msgstr ""
msgid "Personal Access Token"
msgstr ""
......@@ -2182,9 +2239,30 @@ msgstr ""
msgid "Pipelines|Build with confidence"
msgstr ""
msgid "Pipelines|CI Lint"
msgstr ""
msgid "Pipelines|Clear Runner Caches"
msgstr ""
msgid "Pipelines|Get started with Pipelines"
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"
msgstr ""
......@@ -2437,6 +2515,9 @@ msgstr ""
msgid "Public - The project can be accessed without any authentication."
msgstr ""
msgid "Push access to this project is necessary in order to enable this option"
msgstr ""
msgid "Push events"
msgstr ""
......@@ -2523,6 +2604,9 @@ msgstr ""
msgid "Revert this merge request"
msgstr ""
msgid "Running"
msgstr ""
msgid "SSH Keys"
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"
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."
msgstr ""
......@@ -3570,6 +3657,9 @@ msgstr[1] ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr ""
msgid "mrWidget|Allows edits from maintainers"
msgstr ""
msgid "mrWidget|Cancel automatic merge"
msgstr ""
......
......@@ -98,10 +98,8 @@ describe Projects::MilestonesController do
it 'shows group milestone' do
post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
group_milestone = assigns(:milestone)
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.')
expect(flash[:notice]).to eq("#{milestone.title} promoted to group milestone")
expect(response).to redirect_to(project_milestones_path(project))
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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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