Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
107b28be
Commit
107b28be
authored
Mar 15, 2021
by
Jacques Erasmus
Committed by
Brandon Labuschagne
Mar 15, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add the ability to cherry pick accross forks
Added the ability to cherry-pick accross forks
parent
4e4ca479
Changes
23
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
463 additions
and
18 deletions
+463
-18
app/assets/javascripts/projects/commit/components/branches_dropdown.vue
...ascripts/projects/commit/components/branches_dropdown.vue
+19
-4
app/assets/javascripts/projects/commit/components/form_modal.vue
...ets/javascripts/projects/commit/components/form_modal.vue
+29
-1
app/assets/javascripts/projects/commit/components/projects_dropdown.vue
...ascripts/projects/commit/components/projects_dropdown.vue
+87
-0
app/assets/javascripts/projects/commit/constants.js
app/assets/javascripts/projects/commit/constants.js
+8
-5
app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
...ascripts/projects/commit/init_cherry_pick_commit_modal.js
+7
-1
app/assets/javascripts/projects/commit/store/actions.js
app/assets/javascripts/projects/commit/store/actions.js
+18
-2
app/assets/javascripts/projects/commit/store/getters.js
app/assets/javascripts/projects/commit/store/getters.js
+2
-0
app/assets/javascripts/projects/commit/store/mutation_types.js
...ssets/javascripts/projects/commit/store/mutation_types.js
+3
-0
app/assets/javascripts/projects/commit/store/mutations.js
app/assets/javascripts/projects/commit/store/mutations.js
+9
-0
app/assets/javascripts/projects/commit/store/state.js
app/assets/javascripts/projects/commit/store/state.js
+3
-0
app/controllers/projects/commit_controller.rb
app/controllers/projects/commit_controller.rb
+4
-0
app/helpers/commits_helper.rb
app/helpers/commits_helper.rb
+10
-0
app/views/projects/commit/_change.html.haml
app/views/projects/commit/_change.html.haml
+4
-1
changelogs/unreleased/21268-cherry-pick-accross-forks-fe.yml
changelogs/unreleased/21268-cherry-pick-accross-forks-fe.yml
+5
-0
config/feature_flags/development/pick_into_project.yml
config/feature_flags/development/pick_into_project.yml
+8
-0
locale/gitlab.pot
locale/gitlab.pot
+9
-0
spec/frontend/projects/commit/components/form_modal_spec.js
spec/frontend/projects/commit/components/form_modal_spec.js
+17
-3
spec/frontend/projects/commit/components/projects_dropdown_spec.js
...tend/projects/commit/components/projects_dropdown_spec.js
+124
-0
spec/frontend/projects/commit/mock_data.js
spec/frontend/projects/commit/mock_data.js
+1
-0
spec/frontend/projects/commit/store/actions_spec.js
spec/frontend/projects/commit/store/actions_spec.js
+40
-1
spec/frontend/projects/commit/store/getters_spec.js
spec/frontend/projects/commit/store/getters_spec.js
+17
-0
spec/frontend/projects/commit/store/mutations_spec.js
spec/frontend/projects/commit/store/mutations_spec.js
+20
-0
spec/helpers/commits_helper_spec.rb
spec/helpers/commits_helper_spec.rb
+19
-0
No files found.
app/assets/javascripts/projects/commit/components/branches_dropdown.vue
View file @
107b28be
...
@@ -7,7 +7,11 @@ import {
...
@@ -7,7 +7,11 @@ import {
GlLoadingIcon
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
}
from
'
@gitlab/ui
'
;
import
{
mapActions
,
mapGetters
,
mapState
}
from
'
vuex
'
;
import
{
mapActions
,
mapGetters
,
mapState
}
from
'
vuex
'
;
import
{
I18N_DROPDOWN
}
from
'
../constants
'
;
import
{
I18N_NO_RESULTS_MESSAGE
,
I18N_BRANCH_HEADER
,
I18N_BRANCH_SEARCH_PLACEHOLDER
,
}
from
'
../constants
'
;
export
default
{
export
default
{
name
:
'
BranchesDropdown
'
,
name
:
'
BranchesDropdown
'
,
...
@@ -25,7 +29,11 @@ export default {
...
@@ -25,7 +29,11 @@ export default {
default
:
''
,
default
:
''
,
},
},
},
},
i18n
:
I18N_DROPDOWN
,
i18n
:
{
noResultsMessage
:
I18N_NO_RESULTS_MESSAGE
,
branchHeaderTitle
:
I18N_BRANCH_HEADER
,
branchSearchPlaceholder
:
I18N_BRANCH_SEARCH_PLACEHOLDER
,
},
data
()
{
data
()
{
return
{
return
{
searchTerm
:
this
.
value
,
searchTerm
:
this
.
value
,
...
@@ -41,6 +49,13 @@ export default {
...
@@ -41,6 +49,13 @@ export default {
);
);
},
},
},
},
watch
:
{
// Parent component can set the branch value (e.g. when the user selects a different project)
// and we need to keep the search term in sync with the selected value
value
(
val
)
{
this
.
searchTermChanged
(
val
);
},
},
mounted
()
{
mounted
()
{
this
.
fetchBranches
(
this
.
searchTerm
);
this
.
fetchBranches
(
this
.
searchTerm
);
},
},
...
@@ -61,13 +76,13 @@ export default {
...
@@ -61,13 +76,13 @@ export default {
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<gl-dropdown
:text=
"value"
:header-text=
"$options.i18n.
h
eaderTitle"
>
<gl-dropdown
:text=
"value"
:header-text=
"$options.i18n.
branchH
eaderTitle"
>
<gl-search-box-by-type
<gl-search-box-by-type
:value=
"searchTerm"
:value=
"searchTerm"
trim
trim
autocomplete=
"off"
autocomplete=
"off"
:debounce=
"250"
:debounce=
"250"
:placeholder=
"$options.i18n.
s
earchPlaceholder"
:placeholder=
"$options.i18n.
branchS
earchPlaceholder"
data-testid=
"dropdown-search-box"
data-testid=
"dropdown-search-box"
@
input=
"searchTermChanged"
@
input=
"searchTermChanged"
/>
/>
...
...
app/assets/javascripts/projects/commit/components/form_modal.vue
View file @
107b28be
...
@@ -3,18 +3,22 @@ import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab
...
@@ -3,18 +3,22 @@ import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab
import
{
mapActions
,
mapState
}
from
'
vuex
'
;
import
{
mapActions
,
mapState
}
from
'
vuex
'
;
import
{
BV_SHOW_MODAL
}
from
'
~/lib/utils/constants
'
;
import
{
BV_SHOW_MODAL
}
from
'
~/lib/utils/constants
'
;
import
csrf
from
'
~/lib/utils/csrf
'
;
import
csrf
from
'
~/lib/utils/csrf
'
;
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
import
BranchesDropdown
from
'
./branches_dropdown.vue
'
;
import
BranchesDropdown
from
'
./branches_dropdown.vue
'
;
import
ProjectsDropdown
from
'
./projects_dropdown.vue
'
;
export
default
{
export
default
{
components
:
{
components
:
{
BranchesDropdown
,
BranchesDropdown
,
ProjectsDropdown
,
GlModal
,
GlModal
,
GlForm
,
GlForm
,
GlFormCheckbox
,
GlFormCheckbox
,
GlSprintf
,
GlSprintf
,
GlFormGroup
,
GlFormGroup
,
},
},
mixins
:
[
glFeatureFlagsMixin
()],
inject
:
{
inject
:
{
prependedText
:
{
prependedText
:
{
default
:
''
,
default
:
''
,
...
@@ -60,13 +64,17 @@ export default {
...
@@ -60,13 +64,17 @@ export default {
'
modalTitle
'
,
'
modalTitle
'
,
'
existingBranch
'
,
'
existingBranch
'
,
'
prependedText
'
,
'
prependedText
'
,
'
targetProjectId
'
,
'
targetProjectName
'
,
'
branchesEndpoint
'
,
]),
]),
},
},
mounted
()
{
mounted
()
{
this
.
setSelectedProject
(
this
.
targetProjectId
);
eventHub
.
$on
(
this
.
openModal
,
this
.
show
);
eventHub
.
$on
(
this
.
openModal
,
this
.
show
);
},
},
methods
:
{
methods
:
{
...
mapActions
([
'
clearModal
'
,
'
setBranch
'
,
'
setSelectedBranch
'
]),
...
mapActions
([
'
clearModal
'
,
'
setBranch
'
,
'
setSelectedBranch
'
,
'
setSelectedProject
'
]),
show
()
{
show
()
{
this
.
$root
.
$emit
(
BV_SHOW_MODAL
,
this
.
modalId
);
this
.
$root
.
$emit
(
BV_SHOW_MODAL
,
this
.
modalId
);
},
},
...
@@ -101,6 +109,26 @@ export default {
...
@@ -101,6 +109,26 @@ export default {
<gl-form
ref=
"form"
:action=
"endpoint"
method=
"post"
>
<gl-form
ref=
"form"
:action=
"endpoint"
method=
"post"
>
<input
type=
"hidden"
name=
"authenticity_token"
:value=
"$options.csrf.token"
/>
<input
type=
"hidden"
name=
"authenticity_token"
:value=
"$options.csrf.token"
/>
<gl-form-group
v-if=
"glFeatures.pickIntoProject"
:label=
"i18n.projectLabel"
label-for=
"start_project"
data-testid=
"dropdown-group"
>
<input
id=
"target_project_id"
type=
"hidden"
name=
"target_project_id"
:value=
"targetProjectId"
/>
<projects-dropdown
class=
"gl-w-half"
:value=
"targetProjectName"
@
selectProject=
"setSelectedProject"
/>
</gl-form-group>
<gl-form-group
<gl-form-group
:label=
"i18n.branchLabel"
:label=
"i18n.branchLabel"
label-for=
"start_branch"
label-for=
"start_branch"
...
...
app/assets/javascripts/projects/commit/components/projects_dropdown.vue
0 → 100644
View file @
107b28be
<
script
>
import
{
GlDropdown
,
GlSearchBoxByType
,
GlDropdownItem
,
GlDropdownText
}
from
'
@gitlab/ui
'
;
import
{
mapGetters
,
mapState
}
from
'
vuex
'
;
import
{
I18N_NO_RESULTS_MESSAGE
,
I18N_PROJECT_HEADER
,
I18N_PROJECT_SEARCH_PLACEHOLDER
,
}
from
'
../constants
'
;
export
default
{
name
:
'
ProjectsDropdown
'
,
components
:
{
GlDropdown
,
GlSearchBoxByType
,
GlDropdownItem
,
GlDropdownText
,
},
props
:
{
value
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
i18n
:
{
noResultsMessage
:
I18N_NO_RESULTS_MESSAGE
,
projectHeaderTitle
:
I18N_PROJECT_HEADER
,
projectSearchPlaceholder
:
I18N_PROJECT_SEARCH_PLACEHOLDER
,
},
data
()
{
return
{
filterTerm
:
this
.
value
,
};
},
computed
:
{
...
mapGetters
([
'
sortedProjects
'
]),
...
mapState
([
'
targetProjectId
'
]),
filteredResults
()
{
const
lowerCasedFilterTerm
=
this
.
filterTerm
.
toLowerCase
();
return
this
.
sortedProjects
.
filter
((
project
)
=>
project
.
name
.
toLowerCase
().
includes
(
lowerCasedFilterTerm
),
);
},
selectedProject
()
{
return
this
.
sortedProjects
.
find
((
project
)
=>
project
.
id
===
this
.
targetProjectId
)
||
{};
},
},
methods
:
{
selectProject
(
project
)
{
this
.
$emit
(
'
selectProject
'
,
project
.
id
);
this
.
filterTerm
=
project
.
name
;
// when we select a project, we want the dropdown to filter to the selected project
},
isSelected
(
selectedProject
)
{
return
selectedProject
===
this
.
selectedProject
;
},
filterTermChanged
(
value
)
{
this
.
filterTerm
=
value
;
},
},
};
</
script
>
<
template
>
<gl-dropdown
:text=
"selectedProject.name"
:header-text=
"$options.i18n.projectHeaderTitle"
>
<gl-search-box-by-type
:value=
"filterTerm"
trim
autocomplete=
"off"
:placeholder=
"$options.i18n.projectSearchPlaceholder"
data-testid=
"dropdown-search-box"
@
input=
"filterTermChanged"
/>
<gl-dropdown-item
v-for=
"project in filteredResults"
:key=
"project.name"
:name=
"project.name"
:is-checked=
"isSelected(project)"
is-check-item
data-testid=
"dropdown-item"
@
click=
"selectProject(project)"
>
{{
project
.
name
}}
</gl-dropdown-item>
<gl-dropdown-text
v-if=
"!filteredResults.length"
data-testid=
"empty-result-message"
>
<span
class=
"gl-text-gray-500"
>
{{
$options
.
i18n
.
noResultsMessage
}}
</span>
</gl-dropdown-text>
</gl-dropdown>
</
template
>
app/assets/javascripts/projects/commit/constants.js
View file @
107b28be
...
@@ -26,6 +26,7 @@ export const I18N_REVERT_MODAL = {
...
@@ -26,6 +26,7 @@ export const I18N_REVERT_MODAL = {
export
const
I18N_CHERRY_PICK_MODAL
=
{
export
const
I18N_CHERRY_PICK_MODAL
=
{
branchLabel
:
s__
(
'
ChangeTypeAction|Pick into branch
'
),
branchLabel
:
s__
(
'
ChangeTypeAction|Pick into branch
'
),
projectLabel
:
s__
(
'
ChangeTypeAction|Pick into project
'
),
actionPrimaryText
:
s__
(
'
ChangeTypeAction|Cherry-pick
'
),
actionPrimaryText
:
s__
(
'
ChangeTypeAction|Cherry-pick
'
),
};
};
...
@@ -33,10 +34,12 @@ export const PREPENDED_MODAL_TEXT = s__(
...
@@ -33,10 +34,12 @@ export const PREPENDED_MODAL_TEXT = s__(
'
ChangeTypeAction|This will create a new commit in order to revert the existing changes.
'
,
'
ChangeTypeAction|This will create a new commit in order to revert the existing changes.
'
,
);
);
export
const
I18N_DROPDOWN
=
{
export
const
I18N_NO_RESULTS_MESSAGE
=
__
(
'
No matching results
'
);
noResultsMessage
:
__
(
'
No matching results
'
),
headerTitle
:
s__
(
'
ChangeTypeAction|Switch branch
'
),
export
const
I18N_PROJECT_HEADER
=
s__
(
'
ChangeTypeAction|Switch project
'
);
searchPlaceholder
:
s__
(
'
ChangeTypeAction|Search branches
'
),
export
const
I18N_PROJECT_SEARCH_PLACEHOLDER
=
s__
(
'
ChangeTypeAction|Search projects
'
);
};
export
const
I18N_BRANCH_HEADER
=
s__
(
'
ChangeTypeAction|Switch branch
'
);
export
const
I18N_BRANCH_SEARCH_PLACEHOLDER
=
s__
(
'
ChangeTypeAction|Search branches
'
);
export
const
PROJECT_BRANCHES_ERROR
=
__
(
'
Something went wrong while fetching branches
'
);
export
const
PROJECT_BRANCHES_ERROR
=
__
(
'
Something went wrong while fetching branches
'
);
app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
View file @
107b28be
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
{
parseBoolean
}
from
'
~/lib/utils/common_utils
'
;
import
{
parseBoolean
,
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
CommitFormModal
from
'
./components/form_modal.vue
'
;
import
CommitFormModal
from
'
./components/form_modal.vue
'
;
import
{
import
{
I18N_MODAL
,
I18N_MODAL
,
...
@@ -19,21 +19,27 @@ export default function initInviteMembersModal() {
...
@@ -19,21 +19,27 @@ export default function initInviteMembersModal() {
title
,
title
,
endpoint
,
endpoint
,
branch
,
branch
,
targetProjectId
,
targetProjectName
,
pushCode
,
pushCode
,
branchCollaboration
,
branchCollaboration
,
existingBranch
,
existingBranch
,
branchesEndpoint
,
branchesEndpoint
,
projects
,
}
=
el
.
dataset
;
}
=
el
.
dataset
;
const
store
=
createStore
({
const
store
=
createStore
({
endpoint
,
endpoint
,
branchesEndpoint
,
branchesEndpoint
,
branch
,
branch
,
targetProjectId
,
targetProjectName
,
pushCode
:
parseBoolean
(
pushCode
),
pushCode
:
parseBoolean
(
pushCode
),
branchCollaboration
:
parseBoolean
(
branchCollaboration
),
branchCollaboration
:
parseBoolean
(
branchCollaboration
),
defaultBranch
:
branch
,
defaultBranch
:
branch
,
modalTitle
:
title
,
modalTitle
:
title
,
existingBranch
,
existingBranch
,
projects
:
convertObjectPropsToCamelCase
(
JSON
.
parse
(
projects
),
{
deep
:
true
}),
});
});
return
new
Vue
({
return
new
Vue
({
...
...
app/assets/javascripts/projects/commit/store/actions.js
View file @
107b28be
...
@@ -11,6 +11,10 @@ export const requestBranches = ({ commit }) => {
...
@@ -11,6 +11,10 @@ export const requestBranches = ({ commit }) => {
commit
(
types
.
REQUEST_BRANCHES
);
commit
(
types
.
REQUEST_BRANCHES
);
};
};
export
const
setBranchesEndpoint
=
({
commit
},
endpoint
)
=>
{
commit
(
types
.
SET_BRANCHES_ENDPOINT
,
endpoint
);
};
export
const
fetchBranches
=
({
commit
,
dispatch
,
state
},
query
)
=>
{
export
const
fetchBranches
=
({
commit
,
dispatch
,
state
},
query
)
=>
{
dispatch
(
'
requestBranches
'
);
dispatch
(
'
requestBranches
'
);
...
@@ -18,8 +22,8 @@ export const fetchBranches = ({ commit, dispatch, state }, query) => {
...
@@ -18,8 +22,8 @@ export const fetchBranches = ({ commit, dispatch, state }, query) => {
.
get
(
state
.
branchesEndpoint
,
{
.
get
(
state
.
branchesEndpoint
,
{
params
:
{
search
:
query
},
params
:
{
search
:
query
},
})
})
.
then
((
res
)
=>
{
.
then
((
{
data
}
)
=>
{
commit
(
types
.
RECEIVE_BRANCHES_SUCCESS
,
res
.
data
);
commit
(
types
.
RECEIVE_BRANCHES_SUCCESS
,
data
.
Branches
||
[]
);
})
})
.
catch
(()
=>
{
.
catch
(()
=>
{
createFlash
({
message
:
PROJECT_BRANCHES_ERROR
});
createFlash
({
message
:
PROJECT_BRANCHES_ERROR
});
...
@@ -34,3 +38,15 @@ export const setBranch = ({ commit, dispatch }, branch) => {
...
@@ -34,3 +38,15 @@ export const setBranch = ({ commit, dispatch }, branch) => {
export
const
setSelectedBranch
=
({
commit
},
branch
)
=>
{
export
const
setSelectedBranch
=
({
commit
},
branch
)
=>
{
commit
(
types
.
SET_SELECTED_BRANCH
,
branch
);
commit
(
types
.
SET_SELECTED_BRANCH
,
branch
);
};
};
export
const
setSelectedProject
=
({
commit
,
dispatch
,
state
},
id
)
=>
{
let
{
branchesEndpoint
}
=
state
;
if
(
state
.
projects
?.
length
)
{
branchesEndpoint
=
state
.
projects
.
find
((
p
)
=>
p
.
id
===
id
).
refsUrl
;
}
commit
(
types
.
SET_SELECTED_PROJECT
,
id
);
dispatch
(
'
setBranchesEndpoint
'
,
branchesEndpoint
);
dispatch
(
'
fetchBranches
'
);
};
app/assets/javascripts/projects/commit/store/getters.js
View file @
107b28be
...
@@ -3,3 +3,5 @@ import { uniq } from 'lodash';
...
@@ -3,3 +3,5 @@ import { uniq } from 'lodash';
export
const
joinedBranches
=
(
state
)
=>
{
export
const
joinedBranches
=
(
state
)
=>
{
return
uniq
(
state
.
branches
).
sort
();
return
uniq
(
state
.
branches
).
sort
();
};
};
export
const
sortedProjects
=
(
state
)
=>
uniq
(
state
.
projects
).
sort
();
app/assets/javascripts/projects/commit/store/mutation_types.js
View file @
107b28be
export
const
CLEAR_MODAL
=
'
CLEAR_MODAL
'
;
export
const
CLEAR_MODAL
=
'
CLEAR_MODAL
'
;
export
const
SET_BRANCHES_ENDPOINT
=
'
SET_BRANCHES_ENDPOINT
'
;
export
const
REQUEST_BRANCHES
=
'
REQUEST_BRANCHES
'
;
export
const
REQUEST_BRANCHES
=
'
REQUEST_BRANCHES
'
;
export
const
RECEIVE_BRANCHES_SUCCESS
=
'
RECEIVE_BRANCHES_SUCCESS
'
;
export
const
RECEIVE_BRANCHES_SUCCESS
=
'
RECEIVE_BRANCHES_SUCCESS
'
;
export
const
SET_BRANCH
=
'
SET_BRANCH
'
;
export
const
SET_BRANCH
=
'
SET_BRANCH
'
;
export
const
SET_SELECTED_BRANCH
=
'
SET_SELECTED_BRANCH
'
;
export
const
SET_SELECTED_BRANCH
=
'
SET_SELECTED_BRANCH
'
;
export
const
SET_SELECTED_PROJECT
=
'
SET_SELECTED_PROJECT
'
;
app/assets/javascripts/projects/commit/store/mutations.js
View file @
107b28be
import
*
as
types
from
'
./mutation_types
'
;
import
*
as
types
from
'
./mutation_types
'
;
export
default
{
export
default
{
[
types
.
SET_BRANCHES_ENDPOINT
](
state
,
endpoint
)
{
state
.
branchesEndpoint
=
endpoint
;
},
[
types
.
REQUEST_BRANCHES
](
state
)
{
[
types
.
REQUEST_BRANCHES
](
state
)
{
state
.
isFetching
=
true
;
state
.
isFetching
=
true
;
},
},
...
@@ -22,4 +26,9 @@ export default {
...
@@ -22,4 +26,9 @@ export default {
[
types
.
SET_SELECTED_BRANCH
](
state
,
branch
)
{
[
types
.
SET_SELECTED_BRANCH
](
state
,
branch
)
{
state
.
selectedBranch
=
branch
;
state
.
selectedBranch
=
branch
;
},
},
[
types
.
SET_SELECTED_PROJECT
](
state
,
projectId
)
{
state
.
targetProjectId
=
projectId
;
state
.
branch
=
state
.
defaultBranch
;
},
};
};
app/assets/javascripts/projects/commit/store/state.js
View file @
107b28be
...
@@ -3,6 +3,7 @@ export default () => ({
...
@@ -3,6 +3,7 @@ export default () => ({
branchesEndpoint
:
null
,
branchesEndpoint
:
null
,
isFetching
:
false
,
isFetching
:
false
,
branches
:
[],
branches
:
[],
projects
:
[],
selectedBranch
:
''
,
selectedBranch
:
''
,
pushCode
:
false
,
pushCode
:
false
,
branchCollaboration
:
false
,
branchCollaboration
:
false
,
...
@@ -10,4 +11,6 @@ export default () => ({
...
@@ -10,4 +11,6 @@ export default () => ({
existingBranch
:
''
,
existingBranch
:
''
,
defaultBranch
:
''
,
defaultBranch
:
''
,
branch
:
''
,
branch
:
''
,
targetProjectId
:
''
,
targetProjectName
:
''
,
});
});
app/controllers/projects/commit_controller.rb
View file @
107b28be
...
@@ -24,6 +24,10 @@ class Projects::CommitController < Projects::ApplicationController
...
@@ -24,6 +24,10 @@ class Projects::CommitController < Projects::ApplicationController
push_frontend_feature_flag
(
:ci_commit_pipeline_mini_graph_vue
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:ci_commit_pipeline_mini_graph_vue
,
@project
,
default_enabled: :yaml
)
end
end
before_action
do
push_frontend_feature_flag
(
:pick_into_project
)
end
BRANCH_SEARCH_LIMIT
=
1000
BRANCH_SEARCH_LIMIT
=
1000
COMMIT_DIFFS_PER_PAGE
=
75
COMMIT_DIFFS_PER_PAGE
=
75
...
...
app/helpers/commits_helper.rb
View file @
107b28be
...
@@ -134,6 +134,16 @@ module CommitsHelper
...
@@ -134,6 +134,16 @@ module CommitsHelper
end
end
end
end
def
cherry_pick_projects_data
(
project
)
target_projects
(
project
).
map
do
|
project
|
{
id:
project
.
id
.
to_s
,
name:
project
.
full_path
,
refsUrl:
refs_project_path
(
project
)
}
end
end
protected
protected
# Private: Returns a link to a person. If the person has a matching user and
# Private: Returns a link to a person. If the person has a matching user and
...
...
app/views/projects/commit/_change.html.haml
View file @
107b28be
...
@@ -18,7 +18,10 @@
...
@@ -18,7 +18,10 @@
.js-cherry-pick-commit-modal
{
data:
{
title:
title
,
.js-cherry-pick-commit-modal
{
data:
{
title:
title
,
endpoint:
cherry_pick_namespace_project_commit_path
(
commit
,
namespace_id:
@project
.
namespace
.
full_path
,
project_id:
@project
),
endpoint:
cherry_pick_namespace_project_commit_path
(
commit
,
namespace_id:
@project
.
namespace
.
full_path
,
project_id:
@project
),
branch:
@project
.
default_branch
,
branch:
@project
.
default_branch
,
target_project_id:
@project
.
id
,
target_project_name:
@project
.
full_path
,
push_code:
can?
(
current_user
,
:push_code
,
@project
).
to_s
,
push_code:
can?
(
current_user
,
:push_code
,
@project
).
to_s
,
branch_collaboration:
@project
.
branch_allows_collaboration?
(
current_user
,
selected_branch
).
to_s
,
branch_collaboration:
@project
.
branch_allows_collaboration?
(
current_user
,
selected_branch
).
to_s
,
existing_branch:
ERB
::
Util
.
html_escape
(
selected_branch
),
existing_branch:
ERB
::
Util
.
html_escape
(
selected_branch
),
branches_endpoint:
project_branches_path
(
@project
)
}
}
branches_endpoint:
refs_project_path
(
@project
),
projects:
cherry_pick_projects_data
(
@project
).
to_json
}
}
changelogs/unreleased/21268-cherry-pick-accross-forks-fe.yml
0 → 100644
View file @
107b28be
---
title
:
Add the ability to cherry pick accross forks
merge_request
:
55970
author
:
type
:
added
config/feature_flags/development/pick_into_project.yml
0 → 100644
View file @
107b28be
---
name
:
pick_into_project
introduced_by_url
:
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55970
rollout_issue_url
:
https://gitlab.com/gitlab-org/gitlab/-/issues/324154
milestone
:
'
13.10'
type
:
development
group
:
group::source code
default_enabled
:
false
locale/gitlab.pot
View file @
107b28be
...
@@ -5642,6 +5642,9 @@ msgstr ""
...
@@ -5642,6 +5642,9 @@ msgstr ""
msgid "ChangeTypeAction|Pick into branch"
msgid "ChangeTypeAction|Pick into branch"
msgstr ""
msgstr ""
msgid "ChangeTypeAction|Pick into project"
msgstr ""
msgid "ChangeTypeAction|Revert"
msgid "ChangeTypeAction|Revert"
msgstr ""
msgstr ""
...
@@ -5651,12 +5654,18 @@ msgstr ""
...
@@ -5651,12 +5654,18 @@ msgstr ""
msgid "ChangeTypeAction|Search branches"
msgid "ChangeTypeAction|Search branches"
msgstr ""
msgstr ""
msgid "ChangeTypeAction|Search projects"
msgstr ""
msgid "ChangeTypeAction|Start a %{newMergeRequest} with these changes"
msgid "ChangeTypeAction|Start a %{newMergeRequest} with these changes"
msgstr ""
msgstr ""
msgid "ChangeTypeAction|Switch branch"
msgid "ChangeTypeAction|Switch branch"
msgstr ""
msgstr ""
msgid "ChangeTypeAction|Switch project"
msgstr ""
msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes."
msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes."
msgstr ""
msgstr ""
...
...
spec/frontend/projects/commit/components/form_modal_spec.js
View file @
107b28be
...
@@ -7,6 +7,7 @@ import axios from '~/lib/utils/axios_utils';
...
@@ -7,6 +7,7 @@ import axios from '~/lib/utils/axios_utils';
import
{
BV_SHOW_MODAL
}
from
'
~/lib/utils/constants
'
;
import
{
BV_SHOW_MODAL
}
from
'
~/lib/utils/constants
'
;
import
BranchesDropdown
from
'
~/projects/commit/components/branches_dropdown.vue
'
;
import
BranchesDropdown
from
'
~/projects/commit/components/branches_dropdown.vue
'
;
import
CommitFormModal
from
'
~/projects/commit/components/form_modal.vue
'
;
import
CommitFormModal
from
'
~/projects/commit/components/form_modal.vue
'
;
import
ProjectsDropdown
from
'
~/projects/commit/components/projects_dropdown.vue
'
;
import
eventHub
from
'
~/projects/commit/event_hub
'
;
import
eventHub
from
'
~/projects/commit/event_hub
'
;
import
createStore
from
'
~/projects/commit/store
'
;
import
createStore
from
'
~/projects/commit/store
'
;
import
mockData
from
'
../mock_data
'
;
import
mockData
from
'
../mock_data
'
;
...
@@ -20,7 +21,10 @@ describe('CommitFormModal', () => {
...
@@ -20,7 +21,10 @@ describe('CommitFormModal', () => {
store
=
createStore
({
...
mockData
.
mockModal
,
...
state
});
store
=
createStore
({
...
mockData
.
mockModal
,
...
state
});
wrapper
=
extendedWrapper
(
wrapper
=
extendedWrapper
(
method
(
CommitFormModal
,
{
method
(
CommitFormModal
,
{
provide
,
provide
:
{
...
provide
,
glFeatures
:
{
pickIntoProject
:
true
},
},
propsData
:
{
...
mockData
.
modalPropsData
},
propsData
:
{
...
mockData
.
modalPropsData
},
store
,
store
,
attrs
:
{
attrs
:
{
...
@@ -33,7 +37,9 @@ describe('CommitFormModal', () => {
...
@@ -33,7 +37,9 @@ describe('CommitFormModal', () => {
const
findModal
=
()
=>
wrapper
.
findComponent
(
GlModal
);
const
findModal
=
()
=>
wrapper
.
findComponent
(
GlModal
);
const
findStartBranch
=
()
=>
wrapper
.
find
(
'
#start_branch
'
);
const
findStartBranch
=
()
=>
wrapper
.
find
(
'
#start_branch
'
);
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
BranchesDropdown
);
const
findTargetProject
=
()
=>
wrapper
.
find
(
'
#target_project_id
'
);
const
findBranchesDropdown
=
()
=>
wrapper
.
findComponent
(
BranchesDropdown
);
const
findProjectsDropdown
=
()
=>
wrapper
.
findComponent
(
ProjectsDropdown
);
const
findForm
=
()
=>
findModal
().
findComponent
(
GlForm
);
const
findForm
=
()
=>
findModal
().
findComponent
(
GlForm
);
const
findCheckBox
=
()
=>
findForm
().
findComponent
(
GlFormCheckbox
);
const
findCheckBox
=
()
=>
findForm
().
findComponent
(
GlFormCheckbox
);
const
findPrependedText
=
()
=>
wrapper
.
findByTestId
(
'
prepended-text
'
);
const
findPrependedText
=
()
=>
wrapper
.
findByTestId
(
'
prepended-text
'
);
...
@@ -146,11 +152,19 @@ describe('CommitFormModal', () => {
...
@@ -146,11 +152,19 @@ describe('CommitFormModal', () => {
});
});
it
(
'
Changes the start_branch input value
'
,
async
()
=>
{
it
(
'
Changes the start_branch input value
'
,
async
()
=>
{
findDropdown
().
vm
.
$emit
(
'
selectBranch
'
,
'
_changed_branch_value_
'
);
find
Branches
Dropdown
().
vm
.
$emit
(
'
selectBranch
'
,
'
_changed_branch_value_
'
);
await
wrapper
.
vm
.
$nextTick
();
await
wrapper
.
vm
.
$nextTick
();
expect
(
findStartBranch
().
attributes
(
'
value
'
)).
toBe
(
'
_changed_branch_value_
'
);
expect
(
findStartBranch
().
attributes
(
'
value
'
)).
toBe
(
'
_changed_branch_value_
'
);
});
});
it
(
'
Changes the target_project_id input value
'
,
async
()
=>
{
findProjectsDropdown
().
vm
.
$emit
(
'
selectProject
'
,
'
_changed_project_value_
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
findTargetProject
().
attributes
(
'
value
'
)).
toBe
(
'
_changed_project_value_
'
);
});
});
});
});
});
spec/frontend/projects/commit/components/projects_dropdown_spec.js
0 → 100644
View file @
107b28be
import
{
GlDropdownItem
,
GlSearchBoxByType
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
import
ProjectsDropdown
from
'
~/projects/commit/components/projects_dropdown.vue
'
;
Vue
.
use
(
Vuex
);
describe
(
'
ProjectsDropdown
'
,
()
=>
{
let
wrapper
;
let
store
;
const
spyFetchProjects
=
jest
.
fn
();
const
projectsMockData
=
[
{
id
:
'
1
'
,
name
:
'
_project_1_
'
,
refsUrl
:
'
_project_1_/refs
'
},
{
id
:
'
2
'
,
name
:
'
_project_2_
'
,
refsUrl
:
'
_project_2_/refs
'
},
{
id
:
'
3
'
,
name
:
'
_project_3_
'
,
refsUrl
:
'
_project_3_/refs
'
},
];
const
createComponent
=
(
term
,
state
=
{})
=>
{
store
=
new
Vuex
.
Store
({
getters
:
{
sortedProjects
:
()
=>
projectsMockData
,
},
state
,
});
wrapper
=
extendedWrapper
(
shallowMount
(
ProjectsDropdown
,
{
store
,
propsData
:
{
value
:
term
,
},
}),
);
};
const
findAllDropdownItems
=
()
=>
wrapper
.
findAllComponents
(
GlDropdownItem
);
const
findSearchBoxByType
=
()
=>
wrapper
.
findComponent
(
GlSearchBoxByType
);
const
findDropdownItemByIndex
=
(
index
)
=>
wrapper
.
findAllComponents
(
GlDropdownItem
).
at
(
index
);
const
findNoResults
=
()
=>
wrapper
.
findByTestId
(
'
empty-result-message
'
);
afterEach
(()
=>
{
wrapper
.
destroy
();
spyFetchProjects
.
mockReset
();
});
describe
(
'
No projects found
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
(
'
_non_existent_project_
'
);
});
it
(
'
renders empty results message
'
,
()
=>
{
expect
(
findNoResults
().
text
()).
toBe
(
'
No matching results
'
);
});
it
(
'
shows GlSearchBoxByType with default attributes
'
,
()
=>
{
expect
(
findSearchBoxByType
().
exists
()).
toBe
(
true
);
expect
(
findSearchBoxByType
().
vm
.
$attrs
).
toMatchObject
({
placeholder
:
'
Search projects
'
,
});
});
});
describe
(
'
Search term is empty
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
(
''
);
});
it
(
'
renders all projects when search term is empty
'
,
()
=>
{
expect
(
findAllDropdownItems
()).
toHaveLength
(
3
);
expect
(
findDropdownItemByIndex
(
0
).
text
()).
toBe
(
'
_project_1_
'
);
expect
(
findDropdownItemByIndex
(
1
).
text
()).
toBe
(
'
_project_2_
'
);
expect
(
findDropdownItemByIndex
(
2
).
text
()).
toBe
(
'
_project_3_
'
);
});
it
(
'
should not be selected on the inactive project
'
,
()
=>
{
expect
(
wrapper
.
vm
.
isSelected
(
'
_project_1_
'
)).
toBe
(
false
);
});
});
describe
(
'
Projects found
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
(
'
_project_1_
'
,
{
targetProjectId
:
'
1
'
});
});
it
(
'
renders only the project searched for
'
,
()
=>
{
expect
(
findAllDropdownItems
()).
toHaveLength
(
1
);
expect
(
findDropdownItemByIndex
(
0
).
text
()).
toBe
(
'
_project_1_
'
);
});
it
(
'
should not display empty results message
'
,
()
=>
{
expect
(
findNoResults
().
exists
()).
toBe
(
false
);
});
it
(
'
should signify this project is selected
'
,
()
=>
{
expect
(
findDropdownItemByIndex
(
0
).
props
(
'
isChecked
'
)).
toBe
(
true
);
});
it
(
'
should signify the project is not selected
'
,
()
=>
{
expect
(
wrapper
.
vm
.
isSelected
(
'
_not_selected_project_
'
)).
toBe
(
false
);
});
describe
(
'
Custom events
'
,
()
=>
{
it
(
'
should emit selectProject if a project is clicked
'
,
()
=>
{
findDropdownItemByIndex
(
0
).
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
emitted
(
'
selectProject
'
)).
toEqual
([[
'
1
'
]]);
expect
(
wrapper
.
vm
.
filterTerm
).
toBe
(
'
_project_1_
'
);
});
});
});
describe
(
'
Case insensitive for search term
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
(
'
_PrOjEcT_1_
'
);
});
it
(
'
renders only the project searched for
'
,
()
=>
{
expect
(
findAllDropdownItems
()).
toHaveLength
(
1
);
expect
(
findDropdownItemByIndex
(
0
).
text
()).
toBe
(
'
_project_1_
'
);
});
});
});
spec/frontend/projects/commit/mock_data.js
View file @
107b28be
...
@@ -24,4 +24,5 @@ export default {
...
@@ -24,4 +24,5 @@ export default {
openModal
:
'
_open_modal_
'
,
openModal
:
'
_open_modal_
'
,
},
},
mockBranches
:
[
'
_branch_1
'
,
'
_abc_
'
,
'
_master_
'
],
mockBranches
:
[
'
_branch_1
'
,
'
_abc_
'
,
'
_master_
'
],
mockProjects
:
[
'
_project_1
'
,
'
_abc_
'
,
'
_project_
'
],
};
};
spec/frontend/projects/commit/store/actions_spec.js
View file @
107b28be
...
@@ -47,7 +47,7 @@ describe('Commit form modal store actions', () => {
...
@@ -47,7 +47,7 @@ describe('Commit form modal store actions', () => {
it
(
'
dispatch correct actions on fetchBranches
'
,
(
done
)
=>
{
it
(
'
dispatch correct actions on fetchBranches
'
,
(
done
)
=>
{
jest
jest
.
spyOn
(
axios
,
'
get
'
)
.
spyOn
(
axios
,
'
get
'
)
.
mockImplementation
(()
=>
Promise
.
resolve
({
data
:
mockData
.
mockBranches
}));
.
mockImplementation
(()
=>
Promise
.
resolve
({
data
:
{
Branches
:
mockData
.
mockBranches
}
}));
testAction
(
testAction
(
actions
.
fetchBranches
,
actions
.
fetchBranches
,
...
@@ -108,4 +108,43 @@ describe('Commit form modal store actions', () => {
...
@@ -108,4 +108,43 @@ describe('Commit form modal store actions', () => {
]);
]);
});
});
});
});
describe
(
'
setBranchesEndpoint
'
,
()
=>
{
it
(
'
commits SET_BRANCHES_ENDPOINT mutation
'
,
()
=>
{
const
endpoint
=
'
some/endpoint
'
;
testAction
(
actions
.
setBranchesEndpoint
,
endpoint
,
{},
[
{
type
:
types
.
SET_BRANCHES_ENDPOINT
,
payload
:
endpoint
,
},
]);
});
});
describe
(
'
setSelectedProject
'
,
()
=>
{
const
id
=
1
;
it
(
'
commits SET_SELECTED_PROJECT mutation
'
,
()
=>
{
testAction
(
actions
.
setSelectedProject
,
id
,
{},
[
{
type
:
types
.
SET_SELECTED_PROJECT
,
payload
:
id
,
},
],
[
{
type
:
'
setBranchesEndpoint
'
,
},
{
type
:
'
fetchBranches
'
,
},
],
);
});
});
});
});
spec/frontend/projects/commit/store/getters_spec.js
View file @
107b28be
...
@@ -18,4 +18,21 @@ describe('Commit form modal getters', () => {
...
@@ -18,4 +18,21 @@ describe('Commit form modal getters', () => {
expect
(
getters
.
joinedBranches
(
state
)).
toEqual
(
branches
.
slice
(
1
));
expect
(
getters
.
joinedBranches
(
state
)).
toEqual
(
branches
.
slice
(
1
));
});
});
});
});
describe
(
'
sortedProjects
'
,
()
=>
{
it
(
'
should sort projects with variable branches
'
,
()
=>
{
const
state
=
{
projects
:
mockData
.
mockProjects
,
};
expect
(
getters
.
sortedProjects
(
state
)).
toEqual
(
mockData
.
mockProjects
.
sort
());
});
it
(
'
should provide a uniq list of projects
'
,
()
=>
{
const
projects
=
[
'
_project_
'
,
'
_project_
'
,
'
_some_other_project
'
];
const
state
=
{
projects
};
expect
(
getters
.
sortedProjects
(
state
)).
toEqual
(
projects
.
slice
(
1
));
});
});
});
});
spec/frontend/projects/commit/store/mutations_spec.js
View file @
107b28be
...
@@ -35,6 +35,16 @@ describe('Commit form modal mutations', () => {
...
@@ -35,6 +35,16 @@ describe('Commit form modal mutations', () => {
});
});
});
});
describe
(
'
SET_BRANCHES_ENDPOINT
'
,
()
=>
{
it
(
'
should set branchesEndpoint
'
,
()
=>
{
stateCopy
=
{
branchesEndpoint
:
'
endpoint/1
'
};
mutations
[
types
.
SET_BRANCHES_ENDPOINT
](
stateCopy
,
'
endpoint/2
'
);
expect
(
stateCopy
.
branchesEndpoint
).
toBe
(
'
endpoint/2
'
);
});
});
describe
(
'
SET_BRANCH
'
,
()
=>
{
describe
(
'
SET_BRANCH
'
,
()
=>
{
it
(
'
should set branch
'
,
()
=>
{
it
(
'
should set branch
'
,
()
=>
{
stateCopy
=
{
branch
:
'
_master_
'
};
stateCopy
=
{
branch
:
'
_master_
'
};
...
@@ -54,4 +64,14 @@ describe('Commit form modal mutations', () => {
...
@@ -54,4 +64,14 @@ describe('Commit form modal mutations', () => {
expect
(
stateCopy
.
selectedBranch
).
toBe
(
'
_changed_branch_
'
);
expect
(
stateCopy
.
selectedBranch
).
toBe
(
'
_changed_branch_
'
);
});
});
});
});
describe
(
'
SET_SELECTED_PROJECT
'
,
()
=>
{
it
(
'
should set targetProjectId
'
,
()
=>
{
stateCopy
=
{
targetProjectId
:
'
_project_1_
'
};
mutations
[
types
.
SET_SELECTED_PROJECT
](
stateCopy
,
'
_project_2_
'
);
expect
(
stateCopy
.
targetProjectId
).
toBe
(
'
_project_2_
'
);
});
});
});
});
spec/helpers/commits_helper_spec.rb
View file @
107b28be
...
@@ -3,6 +3,8 @@
...
@@ -3,6 +3,8 @@
require
'spec_helper'
require
'spec_helper'
RSpec
.
describe
CommitsHelper
do
RSpec
.
describe
CommitsHelper
do
include
ProjectForksHelper
describe
'#revert_commit_link'
do
describe
'#revert_commit_link'
do
context
'when current_user exists'
do
context
'when current_user exists'
do
before
do
before
do
...
@@ -239,4 +241,21 @@ RSpec.describe CommitsHelper do
...
@@ -239,4 +241,21 @@ RSpec.describe CommitsHelper do
end
end
end
end
end
end
describe
'#cherry_pick_projects_data'
do
let
(
:project
)
{
create
(
:project
,
:repository
)
}
let
(
:user
)
{
create
(
:user
,
maintainer_projects:
[
project
])
}
let!
(
:forked_project
)
{
fork_project
(
project
,
user
,
{
namespace:
user
.
namespace
,
repository:
true
})
}
before
do
allow
(
helper
).
to
receive
(
:current_user
).
and_return
(
user
)
end
it
'returns data for cherry picking into a project'
do
expect
(
helper
.
cherry_pick_projects_data
(
project
)).
to
match_array
([
{
id:
project
.
id
.
to_s
,
name:
project
.
full_path
,
refsUrl:
refs_project_path
(
project
)
},
{
id:
forked_project
.
id
.
to_s
,
name:
forked_project
.
full_path
,
refsUrl:
refs_project_path
(
forked_project
)
}
])
end
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment