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
6f9f5d80
Commit
6f9f5d80
authored
May 05, 2017
by
Alfredo Sumaran
Committed by
Phil Hughes
May 05, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Create new branch from issue page
parent
d030a2d4
Changes
21
Show whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
730 additions
and
71 deletions
+730
-71
app/assets/javascripts/create_merge_request_dropdown.js
app/assets/javascripts/create_merge_request_dropdown.js
+193
-0
app/assets/javascripts/issue.js
app/assets/javascripts/issue.js
+31
-43
app/assets/stylesheets/pages/issues.scss
app/assets/stylesheets/pages/issues.scss
+83
-0
app/controllers/projects/branches_controller.rb
app/controllers/projects/branches_controller.rb
+21
-13
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+19
-2
app/models/issue.rb
app/models/issue.rb
+8
-0
app/serializers/merge_request_create_entity.rb
app/serializers/merge_request_create_entity.rb
+7
-0
app/serializers/merge_request_create_serializer.rb
app/serializers/merge_request_create_serializer.rb
+3
-0
app/services/merge_requests/create_from_issue_service.rb
app/services/merge_requests/create_from_issue_service.rb
+54
-0
app/views/projects/issues/_new_branch.html.haml
app/views/projects/issues/_new_branch.html.haml
+28
-8
app/views/projects/issues/show.html.haml
app/views/projects/issues/show.html.haml
+5
-2
changelogs/unreleased/28558-create-new-branch-from-issue-page.yml
...gs/unreleased/28558-create-new-branch-from-issue-page.yml
+4
-0
config/routes/project.rb
config/routes/project.rb
+1
-0
spec/controllers/projects/branches_controller_spec.rb
spec/controllers/projects/branches_controller_spec.rb
+37
-1
spec/controllers/projects/issues_controller_spec.rb
spec/controllers/projects/issues_controller_spec.rb
+24
-0
spec/features/issues/create_branch_merge_request_spec.rb
spec/features/issues/create_branch_merge_request_spec.rb
+91
-0
spec/fixtures/api/schemas/branch.json
spec/fixtures/api/schemas/branch.json
+12
-0
spec/fixtures/api/schemas/merge_request.json
spec/fixtures/api/schemas/merge_request.json
+12
-0
spec/javascripts/issue_spec.js
spec/javascripts/issue_spec.js
+2
-2
spec/models/issue_spec.rb
spec/models/issue_spec.rb
+21
-0
spec/services/merge_requests/create_from_issue_service_spec.rb
...services/merge_requests/create_from_issue_service_spec.rb
+74
-0
No files found.
app/assets/javascripts/create_merge_request_dropdown.js
0 → 100644
View file @
6f9f5d80
/* eslint-disable no-new */
/* global Flash */
import
DropLab
from
'
./droplab/drop_lab
'
;
import
ISetter
from
'
./droplab/plugins/input_setter
'
;
// Todo: Remove this when fixing issue in input_setter plugin
const
InputSetter
=
Object
.
assign
({},
ISetter
);
const
CREATE_MERGE_REQUEST
=
'
create-mr
'
;
const
CREATE_BRANCH
=
'
create-branch
'
;
export
default
class
CreateMergeRequestDropdown
{
constructor
(
wrapperEl
)
{
this
.
wrapperEl
=
wrapperEl
;
this
.
createMergeRequestButton
=
this
.
wrapperEl
.
querySelector
(
'
.js-create-merge-request
'
);
this
.
dropdownToggle
=
this
.
wrapperEl
.
querySelector
(
'
.js-dropdown-toggle
'
);
this
.
dropdownList
=
this
.
wrapperEl
.
querySelector
(
'
.dropdown-menu
'
);
this
.
availableButton
=
this
.
wrapperEl
.
querySelector
(
'
.available
'
);
this
.
unavailableButton
=
this
.
wrapperEl
.
querySelector
(
'
.unavailable
'
);
this
.
unavailableButtonArrow
=
this
.
unavailableButton
.
querySelector
(
'
.fa
'
);
this
.
unavailableButtonText
=
this
.
unavailableButton
.
querySelector
(
'
.text
'
);
this
.
createBranchPath
=
this
.
wrapperEl
.
dataset
.
createBranchPath
;
this
.
canCreatePath
=
this
.
wrapperEl
.
dataset
.
canCreatePath
;
this
.
createMrPath
=
this
.
wrapperEl
.
dataset
.
createMrPath
;
this
.
droplabInitialized
=
false
;
this
.
isCreatingMergeRequest
=
false
;
this
.
mergeRequestCreated
=
false
;
this
.
isCreatingBranch
=
false
;
this
.
branchCreated
=
false
;
this
.
init
();
}
init
()
{
this
.
checkAbilityToCreateBranch
();
}
available
()
{
this
.
availableButton
.
classList
.
remove
(
'
hide
'
);
this
.
unavailableButton
.
classList
.
add
(
'
hide
'
);
}
unavailable
()
{
this
.
availableButton
.
classList
.
add
(
'
hide
'
);
this
.
unavailableButton
.
classList
.
remove
(
'
hide
'
);
}
enable
()
{
this
.
createMergeRequestButton
.
classList
.
remove
(
'
disabled
'
);
this
.
createMergeRequestButton
.
removeAttribute
(
'
disabled
'
);
this
.
dropdownToggle
.
classList
.
remove
(
'
disabled
'
);
this
.
dropdownToggle
.
removeAttribute
(
'
disabled
'
);
}
disable
()
{
this
.
createMergeRequestButton
.
classList
.
add
(
'
disabled
'
);
this
.
createMergeRequestButton
.
setAttribute
(
'
disabled
'
,
'
disabled
'
);
this
.
dropdownToggle
.
classList
.
add
(
'
disabled
'
);
this
.
dropdownToggle
.
setAttribute
(
'
disabled
'
,
'
disabled
'
);
}
hide
()
{
this
.
wrapperEl
.
classList
.
add
(
'
hide
'
);
}
setUnavailableButtonState
(
isLoading
=
true
)
{
if
(
isLoading
)
{
this
.
unavailableButtonArrow
.
classList
.
add
(
'
fa-spinner
'
,
'
fa-spin
'
);
this
.
unavailableButtonArrow
.
classList
.
remove
(
'
fa-exclamation-triangle
'
);
this
.
unavailableButtonText
.
textContent
=
'
Checking branch availability…
'
;
}
else
{
this
.
unavailableButtonArrow
.
classList
.
remove
(
'
fa-spinner
'
,
'
fa-spin
'
);
this
.
unavailableButtonArrow
.
classList
.
add
(
'
fa-exclamation-triangle
'
);
this
.
unavailableButtonText
.
textContent
=
'
New branch unavailable
'
;
}
}
checkAbilityToCreateBranch
()
{
return
$
.
ajax
({
type
:
'
GET
'
,
dataType
:
'
json
'
,
url
:
this
.
canCreatePath
,
beforeSend
:
()
=>
this
.
setUnavailableButtonState
(),
})
.
done
((
data
)
=>
{
this
.
setUnavailableButtonState
(
false
);
if
(
data
.
can_create_branch
)
{
this
.
available
();
this
.
enable
();
if
(
!
this
.
droplabInitialized
)
{
this
.
droplabInitialized
=
true
;
this
.
initDroplab
();
this
.
bindEvents
();
}
}
else
if
(
data
.
has_related_branch
)
{
this
.
hide
();
}
}).
fail
(()
=>
{
this
.
unavailable
();
this
.
disable
();
new
Flash
(
'
Failed to check if a new branch can be created.
'
);
});
}
initDroplab
()
{
this
.
droplab
=
new
DropLab
();
this
.
droplab
.
init
(
this
.
dropdownToggle
,
this
.
dropdownList
,
[
InputSetter
],
this
.
getDroplabConfig
());
}
getDroplabConfig
()
{
return
{
InputSetter
:
[{
input
:
this
.
createMergeRequestButton
,
valueAttribute
:
'
data-value
'
,
inputAttribute
:
'
data-action
'
,
},
{
input
:
this
.
createMergeRequestButton
,
valueAttribute
:
'
data-text
'
,
}],
};
}
bindEvents
()
{
this
.
createMergeRequestButton
.
addEventListener
(
'
click
'
,
this
.
onClickCreateMergeRequestButton
.
bind
(
this
));
}
isBusy
()
{
return
this
.
isCreatingMergeRequest
||
this
.
mergeRequestCreated
||
this
.
isCreatingBranch
||
this
.
branchCreated
;
}
onClickCreateMergeRequestButton
(
e
)
{
let
xhr
=
null
;
e
.
preventDefault
();
if
(
this
.
isBusy
())
{
return
;
}
if
(
e
.
target
.
dataset
.
action
===
CREATE_MERGE_REQUEST
)
{
xhr
=
this
.
createMergeRequest
();
}
else
if
(
e
.
target
.
dataset
.
action
===
CREATE_BRANCH
)
{
xhr
=
this
.
createBranch
();
}
xhr
.
fail
(()
=>
{
this
.
isCreatingMergeRequest
=
false
;
this
.
isCreatingBranch
=
false
;
});
xhr
.
always
(()
=>
this
.
enable
());
this
.
disable
();
}
createMergeRequest
()
{
return
$
.
ajax
({
method
:
'
POST
'
,
dataType
:
'
json
'
,
url
:
this
.
createMrPath
,
beforeSend
:
()
=>
(
this
.
isCreatingMergeRequest
=
true
),
})
.
done
((
data
)
=>
{
this
.
mergeRequestCreated
=
true
;
window
.
location
.
href
=
data
.
url
;
})
.
fail
(()
=>
new
Flash
(
'
Failed to create Merge Request. Please try again.
'
));
}
createBranch
()
{
return
$
.
ajax
({
method
:
'
POST
'
,
dataType
:
'
json
'
,
url
:
this
.
createBranchPath
,
beforeSend
:
()
=>
(
this
.
isCreatingBranch
=
true
),
})
.
done
((
data
)
=>
{
this
.
branchCreated
=
true
;
window
.
location
.
href
=
data
.
url
;
})
.
fail
(()
=>
new
Flash
(
'
Failed to create a branch for this issue. Please try again.
'
));
}
}
app/assets/javascripts/issue.js
View file @
6f9f5d80
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
/* global Flash */
/* global Flash */
import
CreateMergeRequestDropdown
from
'
./create_merge_request_dropdown
'
;
require
(
'
./flash
'
);
require
(
'
./flash
'
);
require
(
'
~/lib/utils/text_utility
'
);
require
(
'
~/lib/utils/text_utility
'
);
...
@@ -18,48 +19,49 @@ class Issue {
...
@@ -18,48 +19,49 @@ class Issue {
document
.
querySelector
(
'
#task_status_short
'
).
innerText
=
result
.
task_status_short
;
document
.
querySelector
(
'
#task_status_short
'
).
innerText
=
result
.
task_status_short
;
}
}
});
});
Issue
.
initIssueBtnEventListeners
();
this
.
initIssueBtnEventListeners
();
}
}
Issue
.
$btnNewBranch
=
$
(
'
#new-branch
'
);
Issue
.
$btnNewBranch
=
$
(
'
#new-branch
'
);
Issue
.
createMrDropdownWrap
=
document
.
querySelector
(
'
.create-mr-dropdown-wrap
'
);
Issue
.
initMergeRequests
();
Issue
.
initMergeRequests
();
Issue
.
initRelatedBranches
();
Issue
.
initRelatedBranches
();
Issue
.
initCanCreateBranch
();
if
(
Issue
.
createMrDropdownWrap
)
{
this
.
createMergeRequestDropdown
=
new
CreateMergeRequestDropdown
(
Issue
.
createMrDropdownWrap
);
}
}
}
static
initIssueBtnEventListeners
()
{
initIssueBtnEventListeners
()
{
const
issueFailMessage
=
'
Unable to update this issue at this time.
'
;
const
issueFailMessage
=
'
Unable to update this issue at this time.
'
;
const
closeButtons
=
$
(
'
a.btn-close
'
);
const
closeButtons
=
$
(
'
a.btn-close
'
);
const
isClosedBadge
=
$
(
'
div.status-box-closed
'
);
const
isClosedBadge
=
$
(
'
div.status-box-closed
'
);
const
isOpenBadge
=
$
(
'
div.status-box-open
'
);
const
isOpenBadge
=
$
(
'
div.status-box-open
'
);
const
projectIssuesCounter
=
$
(
'
.issue_counter
'
);
const
projectIssuesCounter
=
$
(
'
.issue_counter
'
);
const
reopenButtons
=
$
(
'
a.btn-reopen
'
);
const
reopenButtons
=
$
(
'
a.btn-reopen
'
);
return
closeButtons
.
add
(
reopenButtons
).
on
(
'
click
'
,
function
(
e
)
{
return
closeButtons
.
add
(
reopenButtons
).
on
(
'
click
'
,
(
e
)
=>
{
var
$
this
,
shouldSubmit
,
url
;
var
$
button
,
shouldSubmit
,
url
;
e
.
preventDefault
();
e
.
preventDefault
();
e
.
stopImmediatePropagation
();
e
.
stopImmediatePropagation
();
$
this
=
$
(
this
);
$
button
=
$
(
e
.
currentTarget
);
shouldSubmit
=
$
this
.
hasClass
(
'
btn-comment
'
);
shouldSubmit
=
$
button
.
hasClass
(
'
btn-comment
'
);
if
(
shouldSubmit
)
{
if
(
shouldSubmit
)
{
Issue
.
submitNoteForm
(
$
this
.
closest
(
'
form
'
));
Issue
.
submitNoteForm
(
$
button
.
closest
(
'
form
'
));
}
}
$this
.
prop
(
'
disabled
'
,
true
);
$button
.
prop
(
'
disabled
'
,
true
);
Issue
.
setNewBranchButtonState
(
true
,
null
);
url
=
$button
.
attr
(
'
href
'
);
url
=
$this
.
attr
(
'
href
'
);
return
$
.
ajax
({
return
$
.
ajax
({
type
:
'
PUT
'
,
type
:
'
PUT
'
,
url
:
url
url
:
url
}).
fail
(
function
(
jqXHR
,
textStatus
,
errorThrown
)
{
})
new
Flash
(
issueFailMessage
);
.
fail
(()
=>
new
Flash
(
issueFailMessage
))
Issue
.
initCanCreateBranch
();
.
done
((
data
)
=>
{
}).
done
(
function
(
data
,
textStatus
,
jqXHR
)
{
if
(
'
id
'
in
data
)
{
if
(
'
id
'
in
data
)
{
$
(
document
).
trigger
(
'
issuable:change
'
);
$
(
document
).
trigger
(
'
issuable:change
'
);
const
isClosed
=
$
this
.
hasClass
(
'
btn-close
'
);
const
isClosed
=
$
button
.
hasClass
(
'
btn-close
'
);
closeButtons
.
toggleClass
(
'
hidden
'
,
isClosed
);
closeButtons
.
toggleClass
(
'
hidden
'
,
isClosed
);
reopenButtons
.
toggleClass
(
'
hidden
'
,
!
isClosed
);
reopenButtons
.
toggleClass
(
'
hidden
'
,
!
isClosed
);
isClosedBadge
.
toggleClass
(
'
hidden
'
,
!
isClosed
);
isClosedBadge
.
toggleClass
(
'
hidden
'
,
!
isClosed
);
...
@@ -68,12 +70,21 @@ class Issue {
...
@@ -68,12 +70,21 @@ class Issue {
let
numProjectIssues
=
Number
(
projectIssuesCounter
.
text
().
replace
(
/
[^\d]
/
,
''
));
let
numProjectIssues
=
Number
(
projectIssuesCounter
.
text
().
replace
(
/
[^\d]
/
,
''
));
numProjectIssues
=
isClosed
?
numProjectIssues
-
1
:
numProjectIssues
+
1
;
numProjectIssues
=
isClosed
?
numProjectIssues
-
1
:
numProjectIssues
+
1
;
projectIssuesCounter
.
text
(
gl
.
text
.
addDelimiter
(
numProjectIssues
));
projectIssuesCounter
.
text
(
gl
.
text
.
addDelimiter
(
numProjectIssues
));
if
(
this
.
createMergeRequestDropdown
)
{
if
(
isClosed
)
{
this
.
createMergeRequestDropdown
.
unavailable
();
this
.
createMergeRequestDropdown
.
disable
();
}
else
{
// We should check in case a branch was created in another tab
this
.
createMergeRequestDropdown
.
checkAbilityToCreateBranch
();
}
}
}
else
{
}
else
{
new
Flash
(
issueFailMessage
);
new
Flash
(
issueFailMessage
);
}
}
$this
.
prop
(
'
disabled
'
,
false
);
$button
.
prop
(
'
disabled
'
,
false
);
Issue
.
initCanCreateBranch
();
});
});
});
});
}
}
...
@@ -109,29 +120,6 @@ class Issue {
...
@@ -109,29 +120,6 @@ class Issue {
}
}
});
});
}
}
static
initCanCreateBranch
()
{
// If the user doesn't have the required permissions the container isn't
// rendered at all.
if
(
Issue
.
$btnNewBranch
.
length
===
0
)
{
return
;
}
return
$
.
getJSON
(
Issue
.
$btnNewBranch
.
data
(
'
path
'
)).
fail
(
function
()
{
Issue
.
setNewBranchButtonState
(
false
,
false
);
new
Flash
(
'
Failed to check if a new branch can be created.
'
);
}).
done
(
function
(
data
)
{
Issue
.
setNewBranchButtonState
(
false
,
data
.
can_create_branch
);
});
}
static
setNewBranchButtonState
(
isPending
,
canCreate
)
{
if
(
Issue
.
$btnNewBranch
.
length
===
0
)
{
return
;
}
Issue
.
$btnNewBranch
.
find
(
'
.available
'
).
toggle
(
!
isPending
&&
canCreate
);
Issue
.
$btnNewBranch
.
find
(
'
.unavailable
'
).
toggle
(
!
isPending
&&
!
canCreate
);
}
}
}
export
default
Issue
;
export
default
Issue
;
app/assets/stylesheets/pages/issues.scss
View file @
6f9f5d80
...
@@ -176,3 +176,86 @@ ul.related-merge-requests > li {
...
@@ -176,3 +176,86 @@ ul.related-merge-requests > li {
.recaptcha
{
.recaptcha
{
margin-bottom
:
30px
;
margin-bottom
:
30px
;
}
}
.new-branch-col
{
padding-top
:
10px
;
}
.create-mr-dropdown-wrap
{
.btn-group
:not
(
.hide
)
{
display
:
flex
;
}
.js-create-merge-request
{
flex-grow
:
1
;
flex-shrink
:
0
;
}
.dropdown-menu
{
width
:
300px
;
opacity
:
1
;
visibility
:
visible
;
transform
:
translateY
(
0
);
display
:
none
;
}
.dropdown-toggle
{
.fa-caret-down
{
pointer-events
:
none
;
margin-left
:
0
;
color
:
inherit
;
margin-left
:
0
;
}
}
li
:not
(
.divider
)
{
padding
:
6px
;
cursor
:
pointer
;
&
:hover
,
&
:focus
{
background-color
:
$dropdown-hover-color
;
color
:
$white-light
;
}
&
.droplab-item-selected
{
.icon-container
{
i
{
visibility
:
visible
;
}
}
}
.icon-container
{
float
:
left
;
padding-left
:
6px
;
i
{
visibility
:
hidden
;
}
}
.description
{
padding-left
:
30px
;
font-size
:
13px
;
strong
{
display
:
block
;
font-weight
:
600
;
}
}
}
}
@media
(
min-width
:
$screen-sm-min
)
{
.new-branch-col
{
padding-top
:
0
;
text-align
:
right
;
}
.create-mr-dropdown-wrap
{
.btn-group
:not
(
.hide
)
{
display
:
inline-block
;
}
}
}
app/controllers/projects/branches_controller.rb
View file @
6f9f5d80
...
@@ -46,16 +46,14 @@ class Projects::BranchesController < Projects::ApplicationController
...
@@ -46,16 +46,14 @@ class Projects::BranchesController < Projects::ApplicationController
SystemNoteService
.
new_issue_branch
(
issue
,
@project
,
current_user
,
branch_name
)
if
issue
SystemNoteService
.
new_issue_branch
(
issue
,
@project
,
current_user
,
branch_name
)
if
issue
end
end
respond_to
do
|
format
|
format
.
html
do
if
result
[
:status
]
==
:success
if
result
[
:status
]
==
:success
@branch
=
result
[
:branch
]
if
redirect_to_autodeploy
if
redirect_to_autodeploy
redirect_to
(
redirect_to
url_to_autodeploy_setup
(
project
,
branch_name
),
url_to_autodeploy_setup
(
project
,
branch_name
),
notice:
view_context
.
autodeploy_flash_notice
(
branch_name
)
notice:
view_context
.
autodeploy_flash_notice
(
branch_name
))
else
else
redirect_to
namespace_project_tree_path
(
@project
.
namespace
,
@project
,
redirect_to
namespace_project_tree_path
(
@project
.
namespace
,
@project
,
branch_name
)
@branch
.
name
)
end
end
else
else
@error
=
result
[
:message
]
@error
=
result
[
:message
]
...
@@ -63,6 +61,16 @@ class Projects::BranchesController < Projects::ApplicationController
...
@@ -63,6 +61,16 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
end
end
format
.
json
do
if
result
[
:status
]
==
:success
render
json:
{
name:
branch_name
,
url:
namespace_project_tree_url
(
@project
.
namespace
,
@project
,
branch_name
)
}
else
render
json:
result
[
:messsage
],
status: :unprocessable_entity
end
end
end
end
def
destroy
def
destroy
@branch_name
=
Addressable
::
URI
.
unescape
(
params
[
:id
])
@branch_name
=
Addressable
::
URI
.
unescape
(
params
[
:id
])
status
=
DeleteBranchService
.
new
(
project
,
current_user
).
execute
(
@branch_name
)
status
=
DeleteBranchService
.
new
(
project
,
current_user
).
execute
(
@branch_name
)
...
...
app/controllers/projects/issues_controller.rb
View file @
6f9f5d80
...
@@ -11,7 +11,7 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -11,7 +11,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action
:redirect_to_external_issue_tracker
,
only:
[
:index
,
:new
]
before_action
:redirect_to_external_issue_tracker
,
only:
[
:index
,
:new
]
before_action
:module_enabled
before_action
:module_enabled
before_action
:issue
,
only:
[
:edit
,
:update
,
:show
,
:referenced_merge_requests
,
before_action
:issue
,
only:
[
:edit
,
:update
,
:show
,
:referenced_merge_requests
,
:related_branches
,
:can_create_branch
,
:rendered_title
]
:related_branches
,
:can_create_branch
,
:rendered_title
,
:create_merge_request
]
# Allow read any issue
# Allow read any issue
before_action
:authorize_read_issue!
,
only:
[
:show
,
:rendered_title
]
before_action
:authorize_read_issue!
,
only:
[
:show
,
:rendered_title
]
...
@@ -22,6 +22,9 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -22,6 +22,9 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow modify issue
# Allow modify issue
before_action
:authorize_update_issue!
,
only:
[
:edit
,
:update
]
before_action
:authorize_update_issue!
,
only:
[
:edit
,
:update
]
# Allow create a new branch and empty WIP merge request from current issue
before_action
:authorize_create_merge_request!
,
only:
[
:create_merge_request
]
respond_to
:html
respond_to
:html
def
index
def
index
...
@@ -199,7 +202,7 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -199,7 +202,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to
do
|
format
|
respond_to
do
|
format
|
format
.
json
do
format
.
json
do
render
json:
{
can_create_branch:
can_create
}
render
json:
{
can_create_branch:
can_create
,
has_related_branch:
@issue
.
has_related_branch?
}
end
end
end
end
end
end
...
@@ -209,6 +212,16 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -209,6 +212,16 @@ class Projects::IssuesController < Projects::ApplicationController
render
json:
{
title:
view_context
.
markdown_field
(
@issue
,
:title
)
}
render
json:
{
title:
view_context
.
markdown_field
(
@issue
,
:title
)
}
end
end
def
create_merge_request
result
=
MergeRequests
::
CreateFromIssueService
.
new
(
project
,
current_user
,
issue_iid:
issue
.
iid
).
execute
if
result
[
:status
]
==
:success
render
json:
MergeRequestCreateSerializer
.
new
.
represent
(
result
[
:merge_request
])
else
render
json:
result
[
:messsage
],
status: :unprocessable_entity
end
end
protected
protected
def
issue
def
issue
...
@@ -232,6 +245,10 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -232,6 +245,10 @@ class Projects::IssuesController < Projects::ApplicationController
return
render_404
unless
can?
(
current_user
,
:admin_issue
,
@project
)
return
render_404
unless
can?
(
current_user
,
:admin_issue
,
@project
)
end
end
def
authorize_create_merge_request!
return
render_404
unless
can?
(
current_user
,
:push_code
,
@project
)
&&
@issue
.
can_be_worked_on?
(
current_user
)
end
def
module_enabled
def
module_enabled
return
render_404
unless
@project
.
feature_available?
(
:issues
,
current_user
)
&&
@project
.
default_issues_tracker?
return
render_404
unless
@project
.
feature_available?
(
:issues
,
current_user
)
&&
@project
.
default_issues_tracker?
end
end
...
...
app/models/issue.rb
View file @
6f9f5d80
...
@@ -155,6 +155,14 @@ class Issue < ActiveRecord::Base
...
@@ -155,6 +155,14 @@ class Issue < ActiveRecord::Base
branches_with_iid
-
branches_with_merge_request
branches_with_iid
-
branches_with_merge_request
end
end
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
def
has_related_branch?
project
.
repository
.
branch_names
.
any?
do
|
branch
|
/\A
#{
iid
}
-(?!\d+-stable)/i
=~
branch
end
end
# To allow polymorphism with MergeRequest.
# To allow polymorphism with MergeRequest.
def
source_project
def
source_project
project
project
...
...
app/serializers/merge_request_create_entity.rb
0 → 100644
View file @
6f9f5d80
class
MergeRequestCreateEntity
<
Grape
::
Entity
expose
:iid
expose
:url
do
|
merge_request
|
Gitlab
::
UrlBuilder
.
build
(
merge_request
)
end
end
app/serializers/merge_request_create_serializer.rb
0 → 100644
View file @
6f9f5d80
class
MergeRequestCreateSerializer
<
BaseSerializer
entity
MergeRequestCreateEntity
end
app/services/merge_requests/create_from_issue_service.rb
0 → 100644
View file @
6f9f5d80
module
MergeRequests
class
CreateFromIssueService
<
MergeRequests
::
CreateService
def
execute
return
error
(
'Invalid issue iid'
)
unless
issue_iid
.
present?
&&
issue
.
present?
result
=
CreateBranchService
.
new
(
project
,
current_user
).
execute
(
branch_name
,
ref
)
return
result
if
result
[
:status
]
==
:error
SystemNoteService
.
new_issue_branch
(
issue
,
project
,
current_user
,
branch_name
)
new_merge_request
=
create
(
merge_request
)
if
new_merge_request
.
valid?
success
(
new_merge_request
)
else
error
(
new_merge_request
.
errors
)
end
end
private
def
issue_iid
@isssue_iid
||=
params
.
delete
(
:issue_iid
)
end
def
issue
@issue
||=
IssuesFinder
.
new
(
current_user
,
project_id:
project
.
id
).
find_by
(
iid:
issue_iid
)
end
def
branch_name
@branch_name
||=
issue
.
to_branch_name
end
def
ref
project
.
default_branch
||
'master'
end
def
merge_request
MergeRequests
::
BuildService
.
new
(
project
,
current_user
,
merge_request_params
).
execute
end
def
merge_request_params
{
source_project_id:
project
.
id
,
source_branch:
branch_name
,
target_project_id:
project
.
id
}
end
def
success
(
merge_request
)
super
().
merge
(
merge_request:
merge_request
)
end
end
end
app/views/projects/issues/_new_branch.html.haml
View file @
6f9f5d80
-
if
can?
(
current_user
,
:push_code
,
@project
)
-
if
can?
(
current_user
,
:push_code
,
@project
)
.pull-right
.create-mr-dropdown-wrap
{
data:
{
can_create_path:
can_create_branch_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
),
create_mr_path:
create_merge_request_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
),
create_branch_path:
namespace_project_branches_path
(
@project
.
namespace
,
@project
,
branch_name:
@issue
.
to_branch_name
,
issue_iid:
@issue
.
iid
)
}
}
#new-branch
.new-branch
{
'data-path'
=>
can_create_branch_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
)
}
.btn-group.unavailable
=
link_to
namespace_project_branches_path
(
@project
.
namespace
,
@project
,
branch_name:
@issue
.
to_branch_name
,
issue_iid:
@issue
.
iid
),
%button
.btn.btn-grouped
{
type:
'button'
,
disabled:
'disabled'
}
method: :post
,
class:
'btn btn-new btn-inverted btn-grouped has-tooltip available hide'
,
title:
@issue
.
to_branch_name
do
=
icon
(
'spinner'
,
class:
'fa-spin'
)
New branch
%span
.text
=
link_to
'#'
,
class:
'unavailable btn btn-grouped hide'
,
disabled:
'disabled'
do
Checking branch availability…
=
icon
(
'exclamation-triangle'
)
.btn-group.available.hide
New branch unavailable
%input
.btn.js-create-merge-request.btn-inverted.btn-success
{
type:
'button'
,
value:
'Create a merge request'
,
data:
{
action:
'create-mr'
}
}
%button
.btn.btn-inverted.dropdown-toggle.btn-inverted.btn-success.js-dropdown-toggle
{
type:
'button'
,
data:
{
'dropdown-trigger'
=>
'#create-merge-request-dropdown'
}
}
=
icon
(
'caret-down'
)
%ul
#create-merge-request-dropdown
.dropdown-menu.dropdown-menu-align-right
{
data:
{
dropdown:
true
}
}
%li
.droplab-item-selected
{
role:
'button'
,
data:
{
value:
'create-mr'
,
'text'
=>
'Create a merge request'
}
}
.menu-item
.icon-container
=
icon
(
'check'
)
.description
%strong
Create a merge request
%span
Creates a branch named after this issue and a merge request. The source branch is '
#{
@project
.
default_branch
}
' by default.
%li
.divider.droplab-item-ignore
%li
{
role:
'button'
,
data:
{
value:
'create-branch'
,
'text'
=>
'Create a branch'
}
}
.menu-item
.icon-container
=
icon
(
'check'
)
.description
%strong
Create a branch
%span
Creates a branch named after this issue. The source branch is '
#{
@project
.
default_branch
}
' by default.
app/views/projects/issues/show.html.haml
View file @
6f9f5d80
...
@@ -70,8 +70,11 @@
...
@@ -70,8 +70,11 @@
// This element is filled in using JavaScript.
// This element is filled in using JavaScript.
.content-block.content-block-small
.content-block.content-block-small
=
render
'new_branch'
unless
@issue
.
confidential?
.row
.col-sm-6
=
render
'award_emoji/awards_block'
,
awardable:
@issue
,
inline:
true
=
render
'award_emoji/awards_block'
,
awardable:
@issue
,
inline:
true
.col-sm-6.new-branch-col
=
render
'new_branch'
unless
@issue
.
confidential?
%section
.issuable-discussion
%section
.issuable-discussion
=
render
'projects/issues/discussion'
=
render
'projects/issues/discussion'
...
...
changelogs/unreleased/28558-create-new-branch-from-issue-page.yml
0 → 100644
View file @
6f9f5d80
---
title
:
Allow to create new branch and empty WIP merge request from issue page
merge_request
:
author
:
config/routes/project.rb
View file @
6f9f5d80
...
@@ -264,6 +264,7 @@ constraints(ProjectUrlConstrainer.new) do
...
@@ -264,6 +264,7 @@ constraints(ProjectUrlConstrainer.new) do
get
:related_branches
get
:related_branches
get
:can_create_branch
get
:can_create_branch
get
:rendered_title
get
:rendered_title
post
:create_merge_request
end
end
collection
do
collection
do
post
:bulk_update
post
:bulk_update
...
...
spec/controllers/projects/branches_controller_spec.rb
View file @
6f9f5d80
...
@@ -14,7 +14,7 @@ describe Projects::BranchesController do
...
@@ -14,7 +14,7 @@ describe Projects::BranchesController do
controller
.
instance_variable_set
(
:@project
,
project
)
controller
.
instance_variable_set
(
:@project
,
project
)
end
end
describe
"POST create"
do
describe
"POST create
with HTML format
"
do
render_views
render_views
context
"on creation of a new branch"
do
context
"on creation of a new branch"
do
...
@@ -152,6 +152,42 @@ describe Projects::BranchesController do
...
@@ -152,6 +152,42 @@ describe Projects::BranchesController do
end
end
end
end
describe
'POST create with JSON format'
do
before
do
sign_in
(
user
)
end
context
'with valid params'
do
it
'returns a successful 200 response'
do
create_branch
name:
'my-branch'
,
ref:
'master'
expect
(
response
).
to
have_http_status
(
200
)
end
it
'returns the created branch'
do
create_branch
name:
'my-branch'
,
ref:
'master'
expect
(
response
).
to
match_response_schema
(
'branch'
)
end
end
context
'with invalid params'
do
it
'returns an unprocessable entity 422 response'
do
create_branch
name:
"<script>alert('merge');</script>"
,
ref:
"<script>alert('ref');</script>"
expect
(
response
).
to
have_http_status
(
422
)
end
end
def
create_branch
(
name
:,
ref
:)
post
:create
,
namespace_id:
project
.
namespace
.
to_param
,
project_id:
project
.
to_param
,
branch_name:
name
,
ref:
ref
,
format: :json
end
end
describe
"POST destroy with HTML format"
do
describe
"POST destroy with HTML format"
do
render_views
render_views
...
...
spec/controllers/projects/issues_controller_spec.rb
View file @
6f9f5d80
...
@@ -756,4 +756,28 @@ describe Projects::IssuesController do
...
@@ -756,4 +756,28 @@ describe Projects::IssuesController do
expect
(
response
).
to
have_http_status
(
200
)
expect
(
response
).
to
have_http_status
(
200
)
end
end
end
end
describe
'POST create_merge_request'
do
before
do
project
.
add_developer
(
user
)
sign_in
(
user
)
end
it
'creates a new merge request'
do
expect
{
create_merge_request
}.
to
change
(
project
.
merge_requests
,
:count
).
by
(
1
)
end
it
'render merge request as json'
do
create_merge_request
expect
(
response
).
to
match_response_schema
(
'merge_request'
)
end
def
create_merge_request
post
:create_merge_request
,
namespace_id:
project
.
namespace
.
to_param
,
project_id:
project
.
to_param
,
id:
issue
.
to_param
,
format: :json
end
end
end
end
spec/features/issues/
new_branch_button
_spec.rb
→
spec/features/issues/
create_branch_merge_request
_spec.rb
View file @
6f9f5d80
require
'rails_helper'
require
'rails_helper'
feature
'Start new branch from an issue'
,
feature:
true
,
js:
true
do
feature
'Create Branch/Merge Request Dropdown on issue page'
,
feature:
true
,
js:
true
do
let
(
:user
)
{
create
(
:user
)
}
let!
(
:project
)
{
create
(
:project
)
}
let!
(
:project
)
{
create
(
:project
)
}
let!
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
,
title:
'Cherry-Coloured Funk'
)
}
let!
(
:user
)
{
create
(
:user
)}
context
"for team members"
do
context
'for team members'
do
before
do
before
do
project
.
team
<<
[
user
,
:
mast
er
]
project
.
team
<<
[
user
,
:
develop
er
]
login_as
(
user
)
login_as
(
user
)
end
end
it
'
shows the new branch button
'
do
it
'
allows creating a merge request from the issue page
'
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
expect
(
page
).
to
have_css
(
'#new-branch .available'
)
select_dropdown_option
(
'create-mr'
)
wait_for_ajax
expect
(
page
).
to
have_content
(
"created branch 1-cherry-coloured-funk"
)
expect
(
page
).
to
have_content
(
"mentioned in merge request !1"
)
visit
namespace_project_merge_request_path
(
project
.
namespace
,
project
,
MergeRequest
.
first
)
expect
(
page
).
to
have_content
(
'WIP: Resolve "Cherry-Coloured Funk"'
)
expect
(
current_path
).
to
eq
(
namespace_project_merge_request_path
(
project
.
namespace
,
project
,
MergeRequest
.
first
))
end
it
'allows creating a branch from the issue page'
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
select_dropdown_option
(
'create-branch'
)
wait_for_ajax
expect
(
page
).
to
have_selector
(
'.dropdown-toggle-text '
,
text:
'1-cherry-coloured-funk'
)
expect
(
current_path
).
to
eq
namespace_project_tree_path
(
project
.
namespace
,
project
,
'1-cherry-coloured-funk'
)
end
end
context
"when there is a referenced merge request"
do
context
"when there is a referenced merge request"
do
...
@@ -34,29 +55,37 @@ feature 'Start new branch from an issue', feature: true, js: true do
...
@@ -34,29 +55,37 @@ feature 'Start new branch from an issue', feature: true, js: true do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
end
end
it
"hides the new branch button"
do
it
'disables the create branch button'
do
expect
(
page
).
to
have_css
(
'
#new-branch .unavailable
'
)
expect
(
page
).
to
have_css
(
'
.create-mr-dropdown-wrap .unavailable:not(.hide)
'
)
expect
(
page
).
not_to
have_css
(
'#new-branch .available'
)
expect
(
page
).
to
have_css
(
'.create-mr-dropdown-wrap .available.hide'
,
visible:
false
)
expect
(
page
).
to
have_content
/1 Related Merge Request/
expect
(
page
).
to
have_content
/1 Related Merge Request/
end
end
end
end
context
'when issue is confidential'
do
context
'when issue is confidential'
do
it
'
hides the new
branch button'
do
it
'
disables the create
branch button'
do
issue
=
create
(
:issue
,
:confidential
,
project:
project
)
issue
=
create
(
:issue
,
:confidential
,
project:
project
)
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
expect
(
page
).
not_to
have_css
(
'
#new-branch
'
)
expect
(
page
).
not_to
have_css
(
'
.create-mr-dropdown-wrap
'
)
end
end
end
end
end
end
context
'for visitors'
do
context
'for visitors'
do
it
'shows no buttons'
do
before
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
end
expect
(
page
).
not_to
have_css
(
'#new-branch'
)
it
'shows no buttons'
do
expect
(
page
).
not_to
have_selector
(
'.create-mr-dropdown-wrap'
)
end
end
end
end
def
select_dropdown_option
(
option
)
find
(
'.create-mr-dropdown-wrap .dropdown-toggle'
).
click
find
(
"li[data-value='
#{
option
}
']"
).
click
find
(
'.js-create-merge-request'
).
click
end
end
end
spec/fixtures/api/schemas/branch.json
0 → 100644
View file @
6f9f5d80
{
"type"
:
"object"
,
"required"
:
[
"name"
,
"url"
],
"properties"
:
{
"name"
:
{
"type"
:
"string"
},
"url"
:
{
"type"
:
"uri"
}
},
"additionalProperties"
:
false
}
spec/fixtures/api/schemas/merge_request.json
0 → 100644
View file @
6f9f5d80
{
"type"
:
"object"
,
"required"
:
[
"iid"
,
"url"
],
"properties"
:
{
"iid"
:
{
"type"
:
"integer"
},
"url"
:
{
"type"
:
"uri"
}
},
"additionalProperties"
:
false
}
spec/javascripts/issue_spec.js
View file @
6f9f5d80
...
@@ -108,8 +108,8 @@ describe('Issue', function() {
...
@@ -108,8 +108,8 @@ describe('Issue', function() {
expect
(
this
.
$triggeredButton
).
toHaveProp
(
'
disabled
'
,
true
);
expect
(
this
.
$triggeredButton
).
toHaveProp
(
'
disabled
'
,
true
);
expectNewBranchButtonState
(
true
,
false
);
expectNewBranchButtonState
(
true
,
false
);
return
this
.
issueStateDeferred
;
return
this
.
issueStateDeferred
;
}
else
if
(
req
.
url
===
Issue
.
$btnNewBranch
.
data
(
'
path
'
)
)
{
}
else
if
(
req
.
url
===
Issue
.
createMrDropdownWrap
.
dataset
.
canCreatePath
)
{
expect
(
req
.
type
).
toBe
(
'
get
'
);
expect
(
req
.
type
).
toBe
(
'
GET
'
);
expectNewBranchButtonState
(
true
,
false
);
expectNewBranchButtonState
(
true
,
false
);
return
this
.
canCreateBranchDeferred
;
return
this
.
canCreateBranchDeferred
;
}
}
...
...
spec/models/issue_spec.rb
View file @
6f9f5d80
...
@@ -291,6 +291,27 @@ describe Issue, models: true do
...
@@ -291,6 +291,27 @@ describe Issue, models: true do
end
end
end
end
describe
'#has_related_branch?'
do
let
(
:issue
)
{
create
(
:issue
,
title:
"Blue Bell Knoll"
)
}
subject
{
issue
.
has_related_branch?
}
context
'branch found'
do
before
do
allow
(
issue
.
project
.
repository
).
to
receive
(
:branch_names
).
and_return
([
"iceblink-luck"
,
issue
.
to_branch_name
])
end
it
{
is_expected
.
to
eq
true
}
end
context
'branch not found'
do
before
do
allow
(
issue
.
project
.
repository
).
to
receive
(
:branch_names
).
and_return
([
"lazy-calm"
])
end
it
{
is_expected
.
to
eq
false
}
end
end
it_behaves_like
'an editable mentionable'
do
it_behaves_like
'an editable mentionable'
do
subject
{
create
(
:issue
,
project:
create
(
:project
,
:repository
))
}
subject
{
create
(
:issue
,
project:
create
(
:project
,
:repository
))
}
...
...
spec/services/merge_requests/create_from_issue_service_spec.rb
0 → 100644
View file @
6f9f5d80
require
'spec_helper'
describe
MergeRequests
::
CreateFromIssueService
,
services:
true
do
let
(
:project
)
{
create
(
:project
,
:repository
)
}
let
(
:user
)
{
create
(
:user
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
subject
(
:service
)
{
described_class
.
new
(
project
,
user
,
issue_iid:
issue
.
iid
)
}
before
do
project
.
add_developer
(
user
)
end
describe
'#execute'
do
it
'returns an error with invalid issue iid'
do
result
=
described_class
.
new
(
project
,
user
,
issue_iid:
-
1
).
execute
expect
(
result
[
:status
]).
to
eq
:error
expect
(
result
[
:message
]).
to
eq
'Invalid issue iid'
end
it
'delegates issue search to IssuesFinder'
do
expect_any_instance_of
(
IssuesFinder
).
to
receive
(
:execute
).
once
.
and_call_original
described_class
.
new
(
project
,
user
,
issue_iid:
-
1
).
execute
end
it
'delegates the branch creation to CreateBranchService'
do
expect_any_instance_of
(
CreateBranchService
).
to
receive
(
:execute
).
once
.
and_call_original
service
.
execute
end
it
'creates a branch based on issue title'
do
service
.
execute
expect
(
project
.
repository
.
branch_exists?
(
issue
.
to_branch_name
)).
to
be_truthy
end
it
'creates a system note'
do
expect
(
SystemNoteService
).
to
receive
(
:new_issue_branch
).
with
(
issue
,
project
,
user
,
issue
.
to_branch_name
)
service
.
execute
end
it
'creates a merge request'
do
expect
{
service
.
execute
}.
to
change
(
project
.
merge_requests
,
:count
).
by
(
1
)
end
it
'sets the merge request title to: "WIP: Resolves "$issue-title"'
do
result
=
service
.
execute
expect
(
result
[
:merge_request
].
title
).
to
eq
(
"WIP: Resolve
\"
#{
issue
.
title
}
\"
"
)
end
it
'sets the merge request author to current user'
do
result
=
service
.
execute
expect
(
result
[
:merge_request
].
author
).
to
eq
user
end
it
'sets the merge request source branch to the new issue branch'
do
result
=
service
.
execute
expect
(
result
[
:merge_request
].
source_branch
).
to
eq
issue
.
to_branch_name
end
it
'sets the merge request target branch to the project default branch'
do
result
=
service
.
execute
expect
(
result
[
:merge_request
].
target_branch
).
to
eq
project
.
default_branch
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