Merge branch 'fetch-forked-projects-create-mr' into 'master'
Create private merge requests in forks Closes #58583 See merge request gitlab-org/gitlab-ce!29984
This commit is contained in:
commit
a816bad9a4
16 changed files with 599 additions and 15 deletions
|
@ -0,0 +1,58 @@
|
||||||
|
<script>
|
||||||
|
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlDropdown,
|
||||||
|
GlDropdownItem,
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
projects: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
selectedProject: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dropdownText() {
|
||||||
|
if (Object.keys(this.selectedProject).length) {
|
||||||
|
return this.selectedProject.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return __('Select private project');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectProject(project) {
|
||||||
|
this.$emit('click', project);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<gl-dropdown toggle-class="d-flex align-items-center w-100" class="w-100">
|
||||||
|
<template slot="button-content">
|
||||||
|
<span class="str-truncated-100 mr-2">
|
||||||
|
<icon name="lock" />
|
||||||
|
{{ dropdownText }}
|
||||||
|
</span>
|
||||||
|
<icon name="chevron-down" class="ml-auto" />
|
||||||
|
</template>
|
||||||
|
<gl-dropdown-item v-for="project in projects" :key="project.id" @click="selectProject(project)">
|
||||||
|
<icon
|
||||||
|
name="mobile-issue-close"
|
||||||
|
:class="{ icon: project.id !== selectedProject.id }"
|
||||||
|
class="js-active-project-check"
|
||||||
|
/>
|
||||||
|
<span class="ml-1">{{ project.name }}</span>
|
||||||
|
</gl-dropdown-item>
|
||||||
|
</gl-dropdown>
|
||||||
|
</template>
|
|
@ -0,0 +1,136 @@
|
||||||
|
<script>
|
||||||
|
import { GlLink } from '@gitlab/ui';
|
||||||
|
import { __, sprintf } from '../../locale';
|
||||||
|
import createFlash from '../../flash';
|
||||||
|
import Api from '../../api';
|
||||||
|
import state from '../state';
|
||||||
|
import Dropdown from './dropdown.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlLink,
|
||||||
|
Dropdown,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
namespacePath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
projectPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
newForkPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
helpPagePath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
projects: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedProject() {
|
||||||
|
return state.selectedProject;
|
||||||
|
},
|
||||||
|
noForkText() {
|
||||||
|
return sprintf(
|
||||||
|
__(
|
||||||
|
'To protect this issues confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private.',
|
||||||
|
),
|
||||||
|
{ link_start: `<a href="${this.newForkPath}" class="help-link">`, link_end: '</a>' },
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchProjects();
|
||||||
|
this.createBtn = document.querySelector('.js-create-target');
|
||||||
|
this.warningText = document.querySelector('.js-exposed-info-warning');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectProject(project) {
|
||||||
|
if (project) {
|
||||||
|
Object.assign(state, {
|
||||||
|
selectedProject: project,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (project.namespaceFullPath !== this.namespacePath) {
|
||||||
|
this.showWarning();
|
||||||
|
}
|
||||||
|
} else if (this.createBtn) {
|
||||||
|
this.createBtn.setAttribute('disabled', 'disabled');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeProjectData(data) {
|
||||||
|
return data.map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name_with_namespace,
|
||||||
|
pathWithNamespace: p.path_with_namespace,
|
||||||
|
namespaceFullpath: p.namespace.full_path,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
fetchProjects() {
|
||||||
|
Api.projectForks(this.projectPath, {
|
||||||
|
with_merge_requests_enabled: true,
|
||||||
|
min_access_level: 30,
|
||||||
|
visibility: 'private',
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
this.projects = this.normalizeProjectData(data);
|
||||||
|
this.selectProject(this.projects[0]);
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
createFlash(__('Error fetching forked projects. Please try again.'));
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showWarning() {
|
||||||
|
if (this.warningText) {
|
||||||
|
this.warningText.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.createBtn) {
|
||||||
|
this.createBtn.classList.add('btn-warning');
|
||||||
|
this.createBtn.classList.remove('btn-success');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{ __('Project') }}</label>
|
||||||
|
<div>
|
||||||
|
<dropdown
|
||||||
|
v-if="projects.length"
|
||||||
|
:projects="projects"
|
||||||
|
:selected-project="selectedProject"
|
||||||
|
@click="selectProject"
|
||||||
|
/>
|
||||||
|
<p class="text-muted mt-1 mb-0">
|
||||||
|
<template v-if="projects.length">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'To protect this issues confidentiality, a private fork of this project was selected.',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ __('No forks available to you.') }}<br />
|
||||||
|
<span v-html="noForkText"></span>
|
||||||
|
</template>
|
||||||
|
<gl-link :href="helpPagePath" class="help-link" target="_blank">
|
||||||
|
<span class="sr-only">{{ __('Read more') }}</span>
|
||||||
|
<i class="fa fa-question-circle" aria-hidden="true"></i>
|
||||||
|
</gl-link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
30
app/assets/javascripts/confidential_merge_request/index.js
Normal file
30
app/assets/javascripts/confidential_merge_request/index.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { parseBoolean } from '../lib/utils/common_utils';
|
||||||
|
import ProjectFormGroup from './components/project_form_group.vue';
|
||||||
|
import state from './state';
|
||||||
|
|
||||||
|
export function isConfidentialIssue() {
|
||||||
|
return parseBoolean(document.querySelector('.js-create-mr').dataset.isConfidential);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canCreateConfidentialMergeRequest() {
|
||||||
|
return isConfidentialIssue() && Object.keys(state.selectedProject).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function init() {
|
||||||
|
const el = document.getElementById('js-forked-project');
|
||||||
|
|
||||||
|
return new Vue({
|
||||||
|
el,
|
||||||
|
render(h) {
|
||||||
|
return h(ProjectFormGroup, {
|
||||||
|
props: {
|
||||||
|
namespacePath: el.dataset.namespacePath,
|
||||||
|
projectPath: el.dataset.projectPath,
|
||||||
|
newForkPath: el.dataset.newForkPath,
|
||||||
|
helpPagePath: el.dataset.helpPagePath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default Vue.observable({
|
||||||
|
selectedProject: {},
|
||||||
|
});
|
|
@ -5,6 +5,12 @@ import Flash from './flash';
|
||||||
import DropLab from './droplab/drop_lab';
|
import DropLab from './droplab/drop_lab';
|
||||||
import ISetter from './droplab/plugins/input_setter';
|
import ISetter from './droplab/plugins/input_setter';
|
||||||
import { __, sprintf } from './locale';
|
import { __, sprintf } from './locale';
|
||||||
|
import {
|
||||||
|
init as initConfidentialMergeRequest,
|
||||||
|
isConfidentialIssue,
|
||||||
|
canCreateConfidentialMergeRequest,
|
||||||
|
} from './confidential_merge_request';
|
||||||
|
import confidentialMergeRequestState from './confidential_merge_request/state';
|
||||||
|
|
||||||
// Todo: Remove this when fixing issue in input_setter plugin
|
// Todo: Remove this when fixing issue in input_setter plugin
|
||||||
const InputSetter = Object.assign({}, ISetter);
|
const InputSetter = Object.assign({}, ISetter);
|
||||||
|
@ -12,6 +18,17 @@ const InputSetter = Object.assign({}, ISetter);
|
||||||
const CREATE_MERGE_REQUEST = 'create-mr';
|
const CREATE_MERGE_REQUEST = 'create-mr';
|
||||||
const CREATE_BRANCH = 'create-branch';
|
const CREATE_BRANCH = 'create-branch';
|
||||||
|
|
||||||
|
function createEndpoint(projectPath, endpoint) {
|
||||||
|
if (canCreateConfidentialMergeRequest()) {
|
||||||
|
return endpoint.replace(
|
||||||
|
projectPath,
|
||||||
|
confidentialMergeRequestState.selectedProject.pathWithNamespace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
export default class CreateMergeRequestDropdown {
|
export default class CreateMergeRequestDropdown {
|
||||||
constructor(wrapperEl) {
|
constructor(wrapperEl) {
|
||||||
this.wrapperEl = wrapperEl;
|
this.wrapperEl = wrapperEl;
|
||||||
|
@ -42,6 +59,8 @@ export default class CreateMergeRequestDropdown {
|
||||||
this.refIsValid = true;
|
this.refIsValid = true;
|
||||||
this.refsPath = this.wrapperEl.dataset.refsPath;
|
this.refsPath = this.wrapperEl.dataset.refsPath;
|
||||||
this.suggestedRef = this.refInput.value;
|
this.suggestedRef = this.refInput.value;
|
||||||
|
this.projectPath = this.wrapperEl.dataset.projectPath;
|
||||||
|
this.projectId = this.wrapperEl.dataset.projectId;
|
||||||
|
|
||||||
// These regexps are used to replace
|
// These regexps are used to replace
|
||||||
// a backend generated new branch name and its source (ref)
|
// a backend generated new branch name and its source (ref)
|
||||||
|
@ -58,6 +77,14 @@ export default class CreateMergeRequestDropdown {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
|
|
||||||
|
if (isConfidentialIssue()) {
|
||||||
|
this.createMergeRequestButton.setAttribute(
|
||||||
|
'data-dropdown-trigger',
|
||||||
|
'#create-merge-request-dropdown',
|
||||||
|
);
|
||||||
|
initConfidentialMergeRequest();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
available() {
|
available() {
|
||||||
|
@ -113,7 +140,9 @@ export default class CreateMergeRequestDropdown {
|
||||||
this.isCreatingBranch = true;
|
this.isCreatingBranch = true;
|
||||||
|
|
||||||
return axios
|
return axios
|
||||||
.post(this.createBranchPath)
|
.post(createEndpoint(this.projectPath, this.createBranchPath), {
|
||||||
|
confidential_issue_project_id: canCreateConfidentialMergeRequest() ? this.projectId : null,
|
||||||
|
})
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
this.branchCreated = true;
|
this.branchCreated = true;
|
||||||
window.location.href = data.url;
|
window.location.href = data.url;
|
||||||
|
@ -125,7 +154,11 @@ export default class CreateMergeRequestDropdown {
|
||||||
this.isCreatingMergeRequest = true;
|
this.isCreatingMergeRequest = true;
|
||||||
|
|
||||||
return axios
|
return axios
|
||||||
.post(this.createMrPath)
|
.post(this.createMrPath, {
|
||||||
|
target_project_id: canCreateConfidentialMergeRequest()
|
||||||
|
? confidentialMergeRequestState.selectedProject.id
|
||||||
|
: null,
|
||||||
|
})
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
this.mergeRequestCreated = true;
|
this.mergeRequestCreated = true;
|
||||||
window.location.href = data.url;
|
window.location.href = data.url;
|
||||||
|
@ -149,6 +182,8 @@ export default class CreateMergeRequestDropdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
|
if (!canCreateConfidentialMergeRequest()) return;
|
||||||
|
|
||||||
this.createMergeRequestButton.classList.remove('disabled');
|
this.createMergeRequestButton.classList.remove('disabled');
|
||||||
this.createMergeRequestButton.removeAttribute('disabled');
|
this.createMergeRequestButton.removeAttribute('disabled');
|
||||||
|
|
||||||
|
@ -205,7 +240,7 @@ export default class CreateMergeRequestDropdown {
|
||||||
if (!ref) return false;
|
if (!ref) return false;
|
||||||
|
|
||||||
return axios
|
return axios
|
||||||
.get(`${this.refsPath}${encodeURIComponent(ref)}`)
|
.get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
const branches = data[Object.keys(data)[0]];
|
const branches = data[Object.keys(data)[0]];
|
||||||
const tags = data[Object.keys(data)[1]];
|
const tags = data[Object.keys(data)[1]];
|
||||||
|
@ -325,6 +360,12 @@ export default class CreateMergeRequestDropdown {
|
||||||
let xhr = null;
|
let xhr = null;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) {
|
||||||
|
this.droplab.hooks.forEach(hook => hook.list.toggle());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isBusy()) {
|
if (this.isBusy()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -287,8 +287,8 @@
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0 1px;
|
padding: 0 1px;
|
||||||
|
|
||||||
a,
|
a:not(.help-link),
|
||||||
button,
|
button:not(.btn),
|
||||||
.menu-item {
|
.menu-item {
|
||||||
@include dropdown-link;
|
@include dropdown-link;
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,7 +169,7 @@ class Projects::BranchesController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def confidential_issue_project
|
def confidential_issue_project
|
||||||
return unless Feature.enabled?(:create_confidential_merge_request, @project)
|
return unless helpers.create_confidential_merge_request_enabled?
|
||||||
return if params[:confidential_issue_project_id].blank?
|
return if params[:confidential_issue_project_id].blank?
|
||||||
|
|
||||||
confidential_issue_project = Project.find(params[:confidential_issue_project_id])
|
confidential_issue_project = Project.find(params[:confidential_issue_project_id])
|
||||||
|
|
|
@ -172,7 +172,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
|
|
||||||
def create_merge_request
|
def create_merge_request
|
||||||
create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
|
create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
|
||||||
create_params[:target_project_id] = params[:target_project_id] if Feature.enabled?(:create_confidential_merge_request, @project)
|
create_params[:target_project_id] = params[:target_project_id] if helpers.create_confidential_merge_request_enabled?
|
||||||
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
|
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
|
||||||
|
|
||||||
if result[:status] == :success
|
if result[:status] == :success
|
||||||
|
|
|
@ -137,7 +137,7 @@ module IssuesHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_confidential_merge_request_enabled?
|
def create_confidential_merge_request_enabled?
|
||||||
Feature.enabled?(:create_confidential_merge_request, @project)
|
Feature.enabled?(:create_confidential_merge_request, @project, default_enabled: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show_new_branch_button?
|
def show_new_branch_button?
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
|
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
|
||||||
- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
|
- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
|
||||||
- value = can_create_confidential_merge_request? ? _('Create confidential merge request') : value
|
- value = can_create_confidential_merge_request? ? _('Create confidential merge request') : value
|
||||||
|
- create_mr_text = can_create_confidential_merge_request? ? _('Create confidential merge request') : _('Create merge request')
|
||||||
|
|
||||||
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
|
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
|
||||||
- create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
|
- create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
|
||||||
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
|
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
|
||||||
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
|
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
|
||||||
|
|
||||||
.create-mr-dropdown-wrap.d-inline-block.full-width-mobile{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
|
.create-mr-dropdown-wrap.d-inline-block.full-width-mobile.js-create-mr{ data: { project_path: @project.full_path, project_id: @project.id, can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path, is_confidential: can_create_confidential_merge_request?.to_s } }
|
||||||
.btn-group.btn-group-sm.unavailable
|
.btn-group.btn-group-sm.unavailable
|
||||||
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
|
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
|
||||||
= icon('spinner', class: 'fa-spin')
|
= icon('spinner', class: 'fa-spin')
|
||||||
|
@ -26,7 +27,7 @@
|
||||||
.droplab-dropdown
|
.droplab-dropdown
|
||||||
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ class: ("create-confidential-merge-request-dropdown-menu" if can_create_confidential_merge_request?), data: { dropdown: true } }
|
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ class: ("create-confidential-merge-request-dropdown-menu" if can_create_confidential_merge_request?), data: { dropdown: true } }
|
||||||
- if can_create_merge_request
|
- if can_create_merge_request
|
||||||
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } }
|
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: create_mr_text } }
|
||||||
.menu-item
|
.menu-item
|
||||||
= icon('check', class: 'icon')
|
= icon('check', class: 'icon')
|
||||||
- if can_create_confidential_merge_request?
|
- if can_create_confidential_merge_request?
|
||||||
|
@ -41,6 +42,8 @@
|
||||||
%li.divider.droplab-item-ignore
|
%li.divider.droplab-item-ignore
|
||||||
|
|
||||||
%li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16
|
%li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16
|
||||||
|
- if can_create_confidential_merge_request?
|
||||||
|
#js-forked-project{ data: { namespace_path: @project.namespace.full_path, project_path: @project.full_path, new_fork_path: new_project_fork_path(@project), help_page_path: help_page_path('user/project/merge_requests') } }
|
||||||
.form-group
|
.form-group
|
||||||
%label{ for: 'new-branch-name' }
|
%label{ for: 'new-branch-name' }
|
||||||
= _('Branch name')
|
= _('Branch name')
|
||||||
|
@ -55,4 +58,8 @@
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
%button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } }
|
%button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } }
|
||||||
= _('Create merge request')
|
= create_mr_text
|
||||||
|
|
||||||
|
- if can_create_confidential_merge_request?
|
||||||
|
%p.text-warning.js-exposed-info-warning.hidden
|
||||||
|
= _('This may expose confidential information as the selected fork is in another namespace that can have other members.')
|
||||||
|
|
|
@ -4204,6 +4204,9 @@ msgstr ""
|
||||||
msgid "Error fetching diverging counts for branches. Please try again."
|
msgid "Error fetching diverging counts for branches. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error fetching forked projects. Please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Error fetching labels."
|
msgid "Error fetching labels."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -6869,6 +6872,9 @@ msgstr ""
|
||||||
msgid "No files found."
|
msgid "No files found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No forks available to you."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "No job trace"
|
msgid "No job trace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -9242,6 +9248,9 @@ msgstr ""
|
||||||
msgid "Select members to invite"
|
msgid "Select members to invite"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select private project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Select project"
|
msgid "Select project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -10815,6 +10824,9 @@ msgstr ""
|
||||||
msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action."
|
msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "This may expose confidential information as the selected fork is in another namespace that can have other members."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "This means you can not push code until you create an empty repository or import existing one."
|
msgid "This means you can not push code until you create an empty repository or import existing one."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -11155,6 +11167,12 @@ msgstr ""
|
||||||
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
|
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "To protect this issues confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "To protect this issues confidentiality, a private fork of this project was selected."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "To see all the user's personal access tokens you must impersonate them first."
|
msgid "To see all the user's personal access tokens you must impersonate them first."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe 'User creates confidential merge request on issue page', :js do
|
||||||
|
include ProjectForksHelper
|
||||||
|
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:project) { create(:project, :repository, :public) }
|
||||||
|
let(:issue) { create(:issue, project: project, confidential: true) }
|
||||||
|
|
||||||
|
def visit_confidential_issue
|
||||||
|
sign_in(user)
|
||||||
|
visit project_issue_path(project, issue)
|
||||||
|
wait_for_requests
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'user has no private fork' do
|
||||||
|
before do
|
||||||
|
fork_project(project, user, repository: true)
|
||||||
|
visit_confidential_issue
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows that user has no fork available' do
|
||||||
|
click_button 'Create confidential merge request'
|
||||||
|
|
||||||
|
page.within '.create-confidential-merge-request-dropdown-menu' do
|
||||||
|
expect(page).to have_content('No forks available to you')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'user has private fork' do
|
||||||
|
let(:forked_project) { fork_project(project, user, repository: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
forked_project.update(visibility: Gitlab::VisibilityLevel::PRIVATE)
|
||||||
|
visit_confidential_issue
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'create merge request in fork' do
|
||||||
|
click_button 'Create confidential merge request'
|
||||||
|
|
||||||
|
page.within '.create-confidential-merge-request-dropdown-menu' do
|
||||||
|
expect(page).to have_button(forked_project.name_with_namespace)
|
||||||
|
click_button 'Create confidential merge request'
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content(forked_project.namespace.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Confidential merge request project form group component renders empty state when response is empty 1`] = `
|
||||||
|
<div
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<label>
|
||||||
|
Project
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!---->
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="text-muted mt-1 mb-0"
|
||||||
|
>
|
||||||
|
|
||||||
|
No forks available to you.
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<span>
|
||||||
|
To protect this issues confidentiality,
|
||||||
|
<a
|
||||||
|
class="help-link"
|
||||||
|
href="https://test.com"
|
||||||
|
>
|
||||||
|
fork the project
|
||||||
|
</a>
|
||||||
|
and set the forks visiblity to private.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<gllink-stub
|
||||||
|
class="help-link"
|
||||||
|
href="/help"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="sr-only"
|
||||||
|
>
|
||||||
|
Read more
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<i
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fa fa-question-circle"
|
||||||
|
/>
|
||||||
|
</gllink-stub>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Confidential merge request project form group component renders fork dropdown 1`] = `
|
||||||
|
<div
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<label>
|
||||||
|
Project
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!---->
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="text-muted mt-1 mb-0"
|
||||||
|
>
|
||||||
|
|
||||||
|
No forks available to you.
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<span>
|
||||||
|
To protect this issues confidentiality,
|
||||||
|
<a
|
||||||
|
class="help-link"
|
||||||
|
href="https://test.com"
|
||||||
|
>
|
||||||
|
fork the project
|
||||||
|
</a>
|
||||||
|
and set the forks visiblity to private.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<gllink-stub
|
||||||
|
class="help-link"
|
||||||
|
href="/help"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="sr-only"
|
||||||
|
>
|
||||||
|
Read more
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<i
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fa fa-question-circle"
|
||||||
|
/>
|
||||||
|
</gllink-stub>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { GlDropdownItem } from '@gitlab/ui';
|
||||||
|
import Dropdown from '~/confidential_merge_request/components/dropdown.vue';
|
||||||
|
|
||||||
|
let vm;
|
||||||
|
|
||||||
|
function factory(projects = []) {
|
||||||
|
vm = mount(Dropdown, {
|
||||||
|
propsData: {
|
||||||
|
projects,
|
||||||
|
selectedProject: projects[0],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Confidential merge request project dropdown component', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vm.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders dropdown items', () => {
|
||||||
|
factory([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'test',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(vm.findAll(GlDropdownItem).length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders selected project icon', () => {
|
||||||
|
factory([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'test 2',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(vm.find('.js-active-project-check').classes()).not.toContain('icon');
|
||||||
|
expect(
|
||||||
|
vm
|
||||||
|
.findAll('.js-active-project-check')
|
||||||
|
.at(1)
|
||||||
|
.classes(),
|
||||||
|
).toContain('icon');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
|
||||||
|
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
const mockData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name_with_namespace: 'root / gitlab-ce',
|
||||||
|
path_with_namespace: 'root/gitlab-ce',
|
||||||
|
namespace: {
|
||||||
|
full_path: 'root',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name_with_namespace: 'test / gitlab-ce',
|
||||||
|
path_with_namespace: 'test/gitlab-ce',
|
||||||
|
namespace: {
|
||||||
|
full_path: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let vm;
|
||||||
|
let mock;
|
||||||
|
|
||||||
|
function factory(projects = mockData) {
|
||||||
|
mock = new MockAdapter(axios);
|
||||||
|
mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects);
|
||||||
|
|
||||||
|
vm = shallowMount(ProjectFormGroup, {
|
||||||
|
localVue,
|
||||||
|
propsData: {
|
||||||
|
namespacePath: 'gitlab-org',
|
||||||
|
projectPath: 'gitlab-org/gitlab-ce',
|
||||||
|
newForkPath: 'https://test.com',
|
||||||
|
helpPagePath: '/help',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Confidential merge request project form group component', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
vm.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders fork dropdown', () => {
|
||||||
|
factory();
|
||||||
|
|
||||||
|
return localVue.nextTick(() => {
|
||||||
|
expect(vm.element).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets selected project as first fork', () => {
|
||||||
|
factory();
|
||||||
|
|
||||||
|
return localVue.nextTick(() => {
|
||||||
|
expect(vm.vm.selectedProject).toEqual({
|
||||||
|
id: 1,
|
||||||
|
name: 'root / gitlab-ce',
|
||||||
|
pathWithNamespace: 'root/gitlab-ce',
|
||||||
|
namespaceFullpath: 'root',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders empty state when response is empty', () => {
|
||||||
|
factory([]);
|
||||||
|
|
||||||
|
return localVue.nextTick(() => {
|
||||||
|
expect(vm.element).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
|
import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
|
||||||
import { TEST_HOST } from 'spec/test_constants';
|
import { TEST_HOST } from './helpers/test_constants';
|
||||||
|
|
||||||
describe('CreateMergeRequestDropdown', () => {
|
describe('CreateMergeRequestDropdown', () => {
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
|
@ -10,7 +10,7 @@ describe('CreateMergeRequestDropdown', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosMock = new MockAdapter(axios);
|
axiosMock = new MockAdapter(axios);
|
||||||
|
|
||||||
setFixtures(`
|
document.body.innerHTML = `
|
||||||
<div id="dummy-wrapper-element">
|
<div id="dummy-wrapper-element">
|
||||||
<div class="available"></div>
|
<div class="available"></div>
|
||||||
<div class="unavailable">
|
<div class="unavailable">
|
||||||
|
@ -18,11 +18,12 @@ describe('CreateMergeRequestDropdown', () => {
|
||||||
<div class="text"></div>
|
<div class="text"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="js-ref"></div>
|
<div class="js-ref"></div>
|
||||||
|
<div class="js-create-mr"></div>
|
||||||
<div class="js-create-merge-request"></div>
|
<div class="js-create-merge-request"></div>
|
||||||
<div class="js-create-target"></div>
|
<div class="js-create-target"></div>
|
||||||
<div class="js-dropdown-toggle"></div>
|
<div class="js-dropdown-toggle"></div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`;
|
||||||
|
|
||||||
const dummyElement = document.getElementById('dummy-wrapper-element');
|
const dummyElement = document.getElementById('dummy-wrapper-element');
|
||||||
dropdown = new CreateMergeRequestDropdown(dummyElement);
|
dropdown = new CreateMergeRequestDropdown(dummyElement);
|
||||||
|
@ -36,7 +37,7 @@ describe('CreateMergeRequestDropdown', () => {
|
||||||
describe('getRef', () => {
|
describe('getRef', () => {
|
||||||
it('escapes branch names correctly', done => {
|
it('escapes branch names correctly', done => {
|
||||||
const endpoint = `${dropdown.refsPath}contains%23hash`;
|
const endpoint = `${dropdown.refsPath}contains%23hash`;
|
||||||
spyOn(axios, 'get').and.callThrough();
|
jest.spyOn(axios, 'get');
|
||||||
axiosMock.onGet(endpoint).replyOnce({});
|
axiosMock.onGet(endpoint).replyOnce({});
|
||||||
|
|
||||||
dropdown
|
dropdown
|
Loading…
Reference in a new issue