summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG1
-rw-r--r--app/assets/stylesheets/pages/issues.scss2
-rw-r--r--app/controllers/projects/branches_controller.rb21
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/models/issue.rb30
-rw-r--r--app/services/merge_requests/build_service.rb15
-rw-r--r--app/services/system_note_service.rb12
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml5
-rw-r--r--app/views/projects/issues/_related_branches.html.haml15
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb96
-rw-r--r--spec/features/issues/new_branch_button_spec.rb49
-rw-r--r--spec/models/issue_spec.rb17
-rw-r--r--spec/services/system_note_service_spec.rb12
15 files changed, 237 insertions, 43 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 638a4f1d3fe..342fbb7a6d1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,6 +4,7 @@ v 8.6.0 (unreleased)
- Bump gitlab_git to 9.0.3 (Stan Hu)
- Support Golang subpackage fetching (Stan Hu)
- Bump Capybara gem to 2.6.2 (Stan Hu)
+ - New branch button appears on issues where applicable
- Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea)
- Removed the default password from the initial admin account created during
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 73718ff511a..7ac4bc468d6 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -49,7 +49,7 @@ form.edit-issue {
margin: 0;
}
-.merge-requests-title {
+.merge-requests-title, .related-branches-title {
font-size: 16px;
font-weight: 600;
}
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 4db3b3bf23d..43ea717cbd2 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -9,7 +9,7 @@ class Projects::BranchesController < Projects::ApplicationController
@sort = params[:sort] || 'name'
@branches = @repository.branches_sorted_by(@sort)
@branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE)
-
+
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
@@ -23,11 +23,15 @@ class Projects::BranchesController < Projects::ApplicationController
def create
branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name)
- ref = sanitize(strip_tags(params[:ref]))
- ref = Addressable::URI.unescape(ref)
+
result = CreateBranchService.new(project, current_user).
execute(branch_name, ref)
+ if params[:issue_iid]
+ issue = @project.issues.find_by(iid: params[:issue_iid])
+ SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
+ end
+
if result[:status] == :success
@branch = result[:branch]
redirect_to namespace_project_tree_path(@project.namespace, @project,
@@ -49,4 +53,15 @@ class Projects::BranchesController < Projects::ApplicationController
format.js { render status: status[:return_code] }
end
end
+
+ private
+
+ def ref
+ if params[:ref]
+ ref_escaped = sanitize(strip_tags(params[:ref]))
+ Addressable::URI.unescape(ref_escaped)
+ else
+ @project.default_branch
+ end
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b0a03ee45cc..aa7a178dcf4 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -65,6 +65,7 @@ class Projects::IssuesController < Projects::ApplicationController
@notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
@merge_requests = @issue.referenced_merge_requests(current_user)
+ @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch)
respond_with(@issue)
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 5f58c0508fd..781298a63b2 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -87,11 +87,20 @@ class Issue < ActiveRecord::Base
end
def referenced_merge_requests(current_user = nil)
- Gitlab::ReferenceExtractor.lazily do
- [self, *notes].flat_map do |note|
- note.all_references(current_user).merge_requests
- end
- end.sort_by(&:iid)
+ @referenced_merge_requests ||= {}
+ @referenced_merge_requests[current_user] ||= begin
+ Gitlab::ReferenceExtractor.lazily do
+ [self, *notes].flat_map do |note|
+ note.all_references(current_user).merge_requests
+ end
+ end.sort_by(&:iid).uniq
+ end
+ end
+
+ def related_branches
+ self.project.repository.branch_names.select do |branch|
+ branch =~ /\A#{iid}-(?!\d+-stable)/i
+ end
end
# Reset issue events cache
@@ -120,4 +129,15 @@ class Issue < ActiveRecord::Base
note.all_references(current_user).merge_requests
end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
end
+
+ def to_branch_name
+ "#{iid}-#{title.parameterize}"
+ end
+
+ def can_be_worked_on?(current_user)
+ !self.closed? &&
+ !self.project.forked? &&
+ self.related_branches.empty? &&
+ self.closed_by_merge_requests(current_user).empty?
+ end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 954746a39a5..fa34753c4fd 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -47,6 +47,21 @@ module MergeRequests
merge_request.title = merge_request.source_branch.titleize.humanize
end
+ # When your branch name starts with an iid followed by a dash this pattern will
+ # be interpreted as the use wants to close that issue on this project
+ # Pattern example: 112-fix-mep-mep
+ # Will lead to appending `Closes #112` to the description
+ if match = merge_request.source_branch.match(/\A(\d+)-/)
+ iid = match[1]
+ closes_issue = "Closes ##{iid}"
+
+ if merge_request.description.present?
+ merge_request.description << closes_issue.prepend("\n")
+ else
+ merge_request.description = closes_issue
+ end
+ end
+
merge_request
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 58a861ee08e..f09b77c4a57 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -207,6 +207,18 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ # Called when a branch is created from the 'new branch' button on a issue
+ # Example note text:
+ #
+ # "Started branch `201-issue-branch-button`"
+ def self.new_issue_branch(issue, project, author, branch)
+ h = Gitlab::Application.routes.url_helpers
+ link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
+
+ body = "Started branch [`#{branch}`](#{link})"
+ create_note(noteable: issue, project: project, author: author, note: body)
+ end
+
# Called when a Mentionable references a Noteable
#
# noteable - Noteable object being referenced
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index d9868ad1f0a..d6b38b327ff 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -1,4 +1,4 @@
--if @merge_requests.any?
+- if @merge_requests.any?
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
new file mode 100644
index 00000000000..e66e4669d48
--- /dev/null
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -0,0 +1,5 @@
+- if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
+ .pull-right
+ = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn', title: @issue.to_branch_name do
+ = icon('code-fork')
+ New Branch
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
new file mode 100644
index 00000000000..b10cd03515f
--- /dev/null
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -0,0 +1,15 @@
+- if @related_branches.any?
+ %h2.related-branches-title
+ = pluralize(@related_branches.count, 'Related Branch')
+ %ul.unstyled-list
+ - @related_branches.each do |branch|
+ %li
+ - sha = @project.repository.find_branch(branch).target
+ - ci_commit = @project.ci_commit(sha) if sha
+ - if ci_commit
+ %span.related-branch-ci-status
+ = render_ci_status(ci_commit)
+ %span.related-branch-info
+ %strong
+ = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
+ = branch
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 0242276cd84..1e8308277cc 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -70,8 +70,10 @@
.merge-requests
= render 'merge_requests'
+ = render 'related_branches'
.content-block.content-block-small
+ = render 'new_branch'
= render 'votes/votes_block', votable: @issue
.row
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 8e06d4bdc77..98ae424ed7c 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -17,49 +17,79 @@ describe Projects::BranchesController do
describe "POST create" do
render_views
- before do
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- branch_name: branch,
- ref: ref
- end
+ context "on creation of a new branch" do
+ before do
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ ref: ref
+ end
- context "valid branch name, valid source" do
- let(:branch) { "merge_branch" }
- let(:ref) { "master" }
- it 'redirects' do
- expect(subject).
- to redirect_to("/#{project.path_with_namespace}/tree/merge_branch")
+ context "valid branch name, valid source" do
+ let(:branch) { "merge_branch" }
+ let(:ref) { "master" }
+ it 'redirects' do
+ expect(subject).
+ to redirect_to("/#{project.path_with_namespace}/tree/merge_branch")
+ end
+ end
+
+ context "invalid branch name, valid ref" do
+ let(:branch) { "<script>alert('merge');</script>" }
+ let(:ref) { "master" }
+ it 'redirects' do
+ expect(subject).
+ to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');")
+ end
+ end
+
+ context "valid branch name, invalid ref" do
+ let(:branch) { "merge_branch" }
+ let(:ref) { "<script>alert('ref');</script>" }
+ it { is_expected.to render_template('new') }
+ end
+
+ context "invalid branch name, invalid ref" do
+ let(:branch) { "<script>alert('merge');</script>" }
+ let(:ref) { "<script>alert('ref');</script>" }
+ it { is_expected.to render_template('new') }
+ end
+
+ context "valid branch name with encoded slashes" do
+ let(:branch) { "feature%2Ftest" }
+ let(:ref) { "<script>alert('ref');</script>" }
+ it { is_expected.to render_template('new') }
+ it { project.repository.branch_names.include?('feature/test') }
end
end
- context "invalid branch name, valid ref" do
- let(:branch) { "<script>alert('merge');</script>" }
- let(:ref) { "master" }
+ describe "created from the new branch button on issues" do
+ let(:branch) { "1-feature-branch" }
+ let!(:issue) { create(:issue, project: project) }
+
+
it 'redirects' do
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+
expect(subject).
- to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');")
+ to redirect_to("/#{project.path_with_namespace}/tree/1-feature-branch")
end
- end
- context "valid branch name, invalid ref" do
- let(:branch) { "merge_branch" }
- let(:ref) { "<script>alert('ref');</script>" }
- it { is_expected.to render_template('new') }
- end
+ it 'posts a system note' do
+ expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, "1-feature-branch")
- context "invalid branch name, invalid ref" do
- let(:branch) { "<script>alert('merge');</script>" }
- let(:ref) { "<script>alert('ref');</script>" }
- it { is_expected.to render_template('new') }
- end
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+ end
- context "valid branch name with encoded slashes" do
- let(:branch) { "feature%2Ftest" }
- let(:ref) { "<script>alert('ref');</script>" }
- it { is_expected.to render_template('new') }
- it { project.repository.branch_names.include?('feature/test')}
end
end
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
new file mode 100644
index 00000000000..462cb1f8f62
--- /dev/null
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+feature 'Start new branch from an issue', feature: true do
+ let!(:project) { create(:project) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:user) { create(:user)}
+
+ context "for team members" do
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ it 'shown the new branch button', js: false do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).to have_link "New Branch"
+ end
+
+ context "when there is a referenced merge request" do
+ let(:note) do
+ create(:note, :on_issue, :system, project: project,
+ note: "mentioned in !#{referenced_mr.iid}")
+ end
+ let(:referenced_mr) do
+ create(:merge_request, :simple, source_project: project, target_project: project,
+ description: "Fixes ##{issue.iid}")
+ end
+
+ before do
+ issue.notes << note
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it "hides the new branch button", js: true do
+ expect(page).not_to have_link "New Branch"
+ expect(page).to have_content /1 Related Merge Request/
+ end
+ end
+ end
+
+ context "for visiters" do
+ it 'no button is shown', js: false do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ expect(page).not_to have_link "New Branch"
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 7f44ca2f7db..2ccdec1eeff 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -130,6 +130,15 @@ describe Issue, models: true do
end
end
+ describe '#related_branches' do
+ it "should " do
+ allow(subject.project.repository).to receive(:branch_names).
+ and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name])
+
+ expect(subject.related_branches).to eq [subject.to_branch_name]
+ end
+ end
+
it_behaves_like 'an editable mentionable' do
subject { create(:issue) }
@@ -140,4 +149,12 @@ describe Issue, models: true do
it_behaves_like 'a Taskable' do
let(:subject) { create :issue }
end
+
+ describe "#to_branch_name" do
+ let(:issue) { build(:issue, title: 'a' * 30) }
+
+ it "starts with the issue iid" do
+ expect(issue.to_branch_name).to match /\A#{issue.iid}-a+\z/
+ end
+ end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5dcc39f5fdc..8e6292014d4 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -280,6 +280,18 @@ describe SystemNoteService, services: true do
end
end
+ describe '.new_issue_branch' do
+ subject { described_class.new_issue_branch(noteable, project, author, "1-mepmep") }
+
+ it_behaves_like 'a system note'
+
+ context 'when a branch is created from the new branch button' do
+ it 'sets the note text' do
+ expect(subject.note).to match /\AStarted branch [`1-mepmep`]/
+ end
+ end
+ end
+
describe '.cross_reference' do
subject { described_class.cross_reference(noteable, mentioner, author) }