mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-11 05:36:07 +10:00
Merge branch 'new-branch-button-issue' into 'master'
New branch button on issues Creates a button which creates a branch for you, referencing the original issue in the branch name. When creating the MR the text `Closes #iid` is appended to the description field. The button; with styling  Links to the related branches on the issue  Styled like the MR links  Please provide input on the following; the CI repo on the GDK had the following happening with the current way I implemented the matching:  Closes #3886 cc @DouweM See merge request !2808
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -49,7 +49,7 @@ form.edit-issue {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.merge-requests-title {
|
||||
.merge-requests-title, .related-branches-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+26
-5
@@ -87,11 +87,21 @@ 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
|
||||
return [] if self.project.empty_repo?
|
||||
self.project.repository.branch_names.select do |branch|
|
||||
branch =~ /\A#{iid}-(?!\d+-stable)/i
|
||||
end
|
||||
end
|
||||
|
||||
# Reset issue events cache
|
||||
@@ -120,4 +130,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user