diff --git a/app/assets/javascripts/branches/components/branch_more_actions.vue b/app/assets/javascripts/branches/components/branch_more_actions.vue new file mode 100644 index 0000000000000000000000000000000000000000..c646dab276086c009486611335ea8bd6454d78f8 --- /dev/null +++ b/app/assets/javascripts/branches/components/branch_more_actions.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/app/assets/javascripts/branches/components/delete_branch_button.vue b/app/assets/javascripts/branches/components/delete_branch_button.vue deleted file mode 100644 index 6a6d4d48c52cb8c74c45e9036a66f062e181be37..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/branches/components/delete_branch_button.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - - - diff --git a/app/assets/javascripts/branches/components/delete_merged_branches.vue b/app/assets/javascripts/branches/components/delete_merged_branches.vue index d9d8f1d742d70fd392e8718219848d2a1366d039..117c15be907cef7555ff3f151d4c9fdee17d541e 100644 --- a/app/assets/javascripts/branches/components/delete_merged_branches.vue +++ b/app/assets/javascripts/branches/components/delete_merged_branches.vue @@ -103,8 +103,18 @@ export default { no-caret placement="right" data-qa-selector="delete_merged_branches_dropdown_button" + class="gl-display-none gl-md-display-block!" :items="dropdownItems" /> + + {{ $options.i18n.deleteButtonText }} + - + initDeleteBranchButton(elem)); +document.querySelectorAll('.js-branch-more-actions').forEach((elem) => initBranchMoreActions(elem)); initDeleteBranchModal(); diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 1e17dd586c73ca442bacb9fae8240df637ddbd78..e60544129ff164b5a77d15e5740a8e45e63f96d4 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -27,6 +27,7 @@ def index # Fetch branches for the specified mode fetch_branches_by_mode + fetch_merge_requests_for_branches @refs_pipelines = @project.ci_pipelines.latest_successful_for_refs(@branches.map(&:name)) @merged_branch_names = repository.merged_branch_names(@branches.map(&:name)) @@ -199,6 +200,15 @@ def fetch_branches_by_mode Projects::BranchesByModeService.new(@project, params.merge(sort: @sort, mode: @mode)).execute end + def fetch_merge_requests_for_branches + @related_merge_requests = @project + .source_of_merge_requests + .including_target_project + .by_target_branch(@project.default_branch) + .by_sorted_source_branches(@branches.map(&:name)) + .group_by(&:source_branch) + end + def fetch_branches_for_overview # Here we get one more branch to indicate if there are more data we're not showing limit = @overview_max_branches + 1 diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index a500a695029d6e9a2847429bd56b63d9e75293f5..9fadd5ece14edad8baef09961145c98fb3169f54 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -20,6 +20,27 @@ def access_levels_data(access_levels) end end end + + def merge_request_status(merge_request) + return unless merge_request.present? + return if merge_request.closed? + + if merge_request.open? || merge_request.locked? + variant = :success + variant = :warning if merge_request.draft? + + mr_icon = 'merge-request-open' + mr_status = _('Open') + elsif merge_request.merged? + variant = :info + mr_icon = 'merge' + mr_status = _('Merged') + else + return + end + + { icon: mr_icon, title: "#{mr_status} - #{merge_request.title}", variant: variant } + end end BranchesHelper.prepend_mod_with('BranchesHelper') diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index e4b2b81005c56e7e2826b6dc70c1cd8b38ab06ac..d6e9be11d36f0d039cddf342af0c33b684c2ec7c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -307,6 +307,13 @@ def public_merge_status scope :open_and_closed, -> { with_states(:opened, :closed) } scope :drafts, -> { where(draft: true) } scope :from_source_branches, ->(branches) { where(source_branch: branches) } + scope :by_sorted_source_branches, ->(branches) do + from_source_branches(branches) + .order(source_branch: :asc, id: :desc) + end + scope :including_target_project, -> do + includes(:target_project) + end scope :by_commit_sha, ->(sha) do where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil) end diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index dbc1fe24d963919f1fe4f7f3d3d4e0b19e84976b..adff64fad5ad39f6f334f31548e5441f38e74e0d 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,51 +1,62 @@ - merged = local_assigns.fetch(:merged, false) - commit = @repository.commit(branch.dereferenced_target) -- merge_project = merge_request_source_project_for_project(@project) -%li{ class: "branch-item gl-py-3! js-branch-item js-branch-#{branch.name}", data: { name: branch.name, qa_selector: 'branch_container', qa_name: branch.name } } - .branch-item-content.gl-display-flex.gl-align-items-center.gl-px-3.gl-py-2 - .branch-info - .gl-display-flex.gl-align-items-center - = sprite_icon('branch', size: 12, css_class: 'gl-flex-shrink-0') - = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name gl-ml-3', data: { qa_selector: 'branch_link' } do - = branch.name - = clipboard_button(text: branch.name, title: _("Copy branch name")) - - if branch.name == @repository.root_ref - = gl_badge_tag s_('DefaultBranchLabel|default'), { variant: :info, size: :sm }, { class: 'gl-ml-2', data: { qa_selector: 'badge_content' } } - - elsif merged - = gl_badge_tag s_('Branches|merged'), { variant: :info, size: :sm }, { class: 'gl-ml-2', title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref }, data: { toggle: 'tooltip', container: 'body', qa_selector: 'badge_content' } } - - if protected_branch?(@project, branch) - = gl_badge_tag s_('Branches|protected'), { variant: :success, size: :sm }, { class: 'gl-ml-2', data: { qa_selector: 'badge_content' } } - - = render_if_exists 'projects/branches/diverged_from_upstream', branch: branch - - .block-truncated - - if commit - = render 'projects/branches/commit', commit: commit, project: @project - - else - = s_('Branches|Can’t find HEAD commit for this branch') - - - if branch.name != @repository.root_ref - .js-branch-divergence-graph - - .controls.d-none.d-md-block< - - if commit_status - = render 'ci/status/icon', size: 24, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5' - - elsif show_commit_status - .gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5 - %svg.s24 - - - if merge_project && create_mr_button?(from: branch.name, source_project: @project) - = render Pajamas::ButtonComponent.new(href: create_mr_path(from: branch.name, source_project: @project)) do - = _('Merge request') - - - if branch.name != @repository.root_ref - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name), - class: "gl-button btn btn-default js-onboarding-compare-branches #{'gl-ml-3' unless merge_project}", - method: :post, - title: s_('Branches|Compare') do - = s_('Branches|Compare') - - = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name], class: 'gl-vertical-align-top' - - - if can?(current_user, :push_code, @project) - = render 'projects/branches/delete_branch_modal_button', project: @project, branch: branch, merged: merged +- related_merge_request = @related_merge_requests[branch.name]&.first +- mr_status = merge_request_status(related_merge_request) +- is_default_branch = branch.name == @repository.root_ref + +%li{ class: "branch-item gl-display-flex! gl-align-items-center! js-branch-item js-branch-#{branch.name} gl-pl-3!", data: { name: branch.name, qa_selector: 'branch_container', qa_name: branch.name } } + .branch-info + .gl-display-flex.gl-align-items-center + = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name', data: { qa_selector: 'branch_link' } do + = branch.name + = clipboard_button(text: branch.name, title: _("Copy branch name")) + - if is_default_branch + = gl_badge_tag s_('DefaultBranchLabel|default'), { variant: :neutral, size: :sm }, { class: 'gl-ml-2', data: { qa_selector: 'badge_content' } } + - if protected_branch?(@project, branch) + = gl_badge_tag s_('Branches|protected'), { variant: :muted, size: :sm }, { class: 'gl-ml-2', data: { qa_selector: 'badge_content' } } + + = render_if_exists 'projects/branches/diverged_from_upstream', branch: branch + + .block-truncated + - if commit + = render 'projects/branches/commit', commit: commit, project: @project + - else + = s_('Branches|Can’t find HEAD commit for this branch') + + - if branch.name != @repository.root_ref + .js-branch-divergence-graph + + .pipeline-status.d-none.d-md-block< + - if commit_status + = render 'ci/status/icon', size: 16, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5' + - elsif show_commit_status + .gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5 + %svg.s16 + + + - if mr_status.present? + .issuable-reference.gl-display-flex.gl-justify-content-end.gl-min-w-10.gl-ml-5.gl-mr-4 + = gl_badge_tag issuable_reference(related_merge_request), + { icon: mr_status[:icon], variant: mr_status[:variant], size: :md, href: merge_request_path(related_merge_request) }, + { class: 'gl-mr-2', title: mr_status[:title], data: { toggle: 'tooltip', container: 'body', qa_selector: 'badge_content' } } + + .controls.d-none.d-md-block< + - if mr_status.nil? && create_mr_button?(from: branch.name, source_project: @project) + = render Pajamas::ButtonComponent.new(icon: 'merge-request', href: create_mr_path(from: branch.name, source_project: @project), button_options: { class: 'has-tooltip gl-mr-2!', title: _('New merge request') }) do + = _('New') + + = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name], css_class: 'gl-mr-1!' + + - if !is_default_branch + .js-branch-more-actions{ data: { + branch_name: branch.name, + default_branch_name: @repository.root_ref, + can_delete_branch: user_access(@project).can_delete_branch?(branch.name).to_s, + is_protected_branch: protected_branch?(@project, branch).to_s, + merged: merged.to_s, + compare_path: project_compare_index_path(@project, from: @repository.root_ref, to: branch.name), + delete_path: project_branch_path(@project, branch.name), + } } + - else + .gl-display-inline-flex.gl-w-7 + diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml index cfa0cf6d07bf96200380d611d7b417db8f19614a..6bbd06175984e566fcd448fe4dcfe5a64c874073 100644 --- a/app/views/projects/branches/_commit.html.haml +++ b/app/views/projects/branches/_commit.html.haml @@ -1,9 +1,7 @@ -.branch-commit.cgray - .icon-container.commit-icon - = custom_icon("icon_commit") +.branch-commit.gl-font-sm.gl-text-gray-500 = link_to commit.short_id, project_commit_path(project, commit.id), class: "commit-sha" · %span.str-truncated - = link_to_markdown commit.title, project_commit_path(project, commit.id), class: "commit-row-message cgray" + = link_to_markdown commit.title, project_commit_path(project, commit.id), class: "commit-row-message gl-text-gray-500!" · %span.gl-text-secondary= time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/branches/_delete_branch_modal_button.html.haml b/app/views/projects/branches/_delete_branch_modal_button.html.haml deleted file mode 100644 index 829a459ad2ca7327dc793de416cabc4ab38ad594..0000000000000000000000000000000000000000 --- a/app/views/projects/branches/_delete_branch_modal_button.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- if branch.name == @project.repository.root_ref - .js-delete-branch-button{ data: { tooltip: s_('Branches|The default branch cannot be deleted'), - disabled: true.to_s } } -- elsif protected_branch?(@project, branch) - - if can?(current_user, :push_to_delete_protected_branch, @project) - .js-delete-branch-button{ data: { branch_name: branch.name, - is_protected_branch: true.to_s, - merged: merged.to_s, - default_branch_name: @project.repository.root_ref, - delete_path: project_branch_path(@project, branch.name) } } - - else - .js-delete-branch-button{ data: { is_protected_branch: true.to_s, - disabled: true.to_s } } -- else - .js-delete-branch-button{ data: { branch_name: branch.name, - merged: merged.to_s, - default_branch_name: @project.repository.root_ref, - delete_path: project_branch_path(@project, branch.name) } } diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 1fbc399c3ff0647c8ac208fdc795663a6d272a33..bbee7d66dcb3f4607835d9cd68622e8d321df97a 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,10 +1,11 @@ - project = local_assigns.fetch(:project) - ref = local_assigns.fetch(:ref) - pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) } +- css_class = local_assigns.fetch(:css_class, '') - if !project.empty_repo? && can?(current_user, :download_code, project) - archive_prefix = "#{project.path}-#{ref.tr('/', '-')}" - .project-action-button.dropdown.gl-dropdown.inline> + .project-action-button.dropdown.gl-dropdown.inline{ class: css_class }> %button.gl-button.btn.btn-default.dropdown-toggle.gl-dropdown-toggle.dropdown-icon-only.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static', data: { qa_selector: 'download_source_code_button' } } = sprite_icon('download', css_class: 'gl-icon dropdown-icon') %span.sr-only= _('Select Archive Format') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0be9d2bbdf16e741f7476a00ce5acf502f0367eb..d3079e82da832c563796356927170743ee643021 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8107,18 +8107,12 @@ msgstr "" msgid "Branches|Filter by branch name" msgstr "" -msgid "Branches|Merged into %{default_branch}" -msgstr "" - msgid "Branches|New branch" msgstr "" msgid "Branches|No branches to show" msgstr "" -msgid "Branches|Only a project maintainer or owner can delete a protected branch" -msgstr "" - msgid "Branches|Overview" msgstr "" @@ -8158,9 +8152,6 @@ msgstr "" msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart." msgstr "" -msgid "Branches|The default branch cannot be deleted" -msgstr "" - msgid "Branches|This branch hasn't been merged into %{defaultBranchName}. To avoid data loss, consider merging this branch before deleting it." msgstr "" @@ -8200,9 +8191,6 @@ msgstr "" msgid "Branches|diverged from upstream" msgstr "" -msgid "Branches|merged" -msgstr "" - msgid "Branches|protected" msgstr "" diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb index a97d0afd1607df3f0b42e78976c29881b5b72805..af328f876f76d4575e5369a8b57c2c366017b0c8 100644 --- a/qa/qa/page/project/branches/show.rb +++ b/qa/qa/page/project/branches/show.rb @@ -5,7 +5,7 @@ module Page module Project module Branches class Show < Page::Base - view 'app/assets/javascripts/branches/components/delete_branch_button.vue' do + view 'app/assets/javascripts/branches/components/branch_more_actions.vue' do element :delete_branch_button end diff --git a/spec/features/projects/branches/user_deletes_branch_spec.rb b/spec/features/projects/branches/user_deletes_branch_spec.rb index d120ea36a555edfe4f08b6cd73dcffd59ddd9a43..7e7ab4b2a474b2c07bff0983e17938822c0aae90 100644 --- a/spec/features/projects/branches/user_deletes_branch_spec.rb +++ b/spec/features/projects/branches/user_deletes_branch_spec.rb @@ -23,7 +23,8 @@ branch_search.native.send_keys(:enter) page.within(".js-branch-improve\\/awesome") do - find('.js-delete-branch-button').click + click_button 'More actions' + find('[data-testid="delete-branch-button"]').click end accept_gl_confirm(button_text: 'Yes, delete branch') diff --git a/spec/features/projects/branches/user_views_branches_spec.rb b/spec/features/projects/branches/user_views_branches_spec.rb index 322e1fa0ac1d48b62a3ad0455991fcf2671d4329..52327cc6543a496f2f9e1414c4ad0d239ba2a60c 100644 --- a/spec/features/projects/branches/user_views_branches_spec.rb +++ b/spec/features/projects/branches/user_views_branches_spec.rb @@ -10,22 +10,41 @@ sign_in(user) end - context "all branches", :js do + context "all branches" do before do visit(project_branches_path(project)) - branch_search = find('input[data-testid="branch-search"]') - branch_search.set('master') - branch_search.native.send_keys(:enter) end - it "shows branches" do - expect(page).to have_content("Branches").and have_content("master") + describe 'default branch' do + before do + search_branches('master') + end - expect(page.all(".graph-side")).to all(have_content(/\d+/)) + it "shows the default branch" do + expect(page).to have_content("Branches").and have_content("master") + + expect(page.all(".graph-side")).to all(have_content(/\d+/)) + end + + it "does not show the \"More actions\" dropdown" do + expect(page).not_to have_selector('[data-testid="branch-more-actions"]') + end end - it "displays a disabled button with a tooltip for the default branch that cannot be deleted", :js do - expect(page).to have_button('The default branch cannot be deleted', disabled: true) + describe 'non-default branch' do + before do + search_branches('feature') + end + + it "shows the branches" do + expect(page).to have_content("Branches").and have_content("feature") + + expect(page.all(".graph-side")).to all(have_content(/\d+/)) + end + + it "shows the \"More actions\" dropdown" do + expect(page).to have_button('More actions') + end end end @@ -42,4 +61,10 @@ end end end + + def search_branches(query) + branch_search = find('input[data-testid="branch-search"]') + branch_search.set(query) + branch_search.native.send_keys(:enter) + end end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 7cfb7240db66e6ab476449d83f583bc74455942e..6a13d5637af3aefbfba7cc9ebc4c88cc6cee4d6c 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -231,7 +231,7 @@ visit project_branches_path(project) page.within first('.all-branches li') do - expect(page).to have_content 'Merge request' + expect(page).to have_content 'New' end end @@ -242,7 +242,7 @@ visit project_branches_path(project) page.within first('.all-branches li') do - expect(page).not_to have_content 'Merge request' + expect(page).not_to have_content 'New' end end @@ -266,7 +266,7 @@ it 'does not show merge request button' do page.within first('.all-branches li') do - expect(page).not_to have_content 'Merge request' + expect(page).not_to have_content 'New' end end end @@ -294,7 +294,7 @@ it 'displays a placeholder when not available' do page.all('.all-branches li') do |li| - expect(li).to have_css 'svg.s24' + expect(li).to have_css '.pipeline-status svg.s16' end end end @@ -306,7 +306,7 @@ it 'does not show placeholder or pipeline status' do page.all('.all-branches') do |branches| - expect(branches).not_to have_css 'svg.s24' + expect(branches).not_to have_css '.pipeline-status svg.s16' end end end @@ -322,6 +322,8 @@ visit project_branches_path(project) page.within first('.all-branches li') do + wait_for_requests + find('[data-testid="branch-more-actions"] .gl-new-dropdown-toggle').click click_link 'Compare' end @@ -329,7 +331,7 @@ end end - context 'on a read-only instance' do + context 'on a read-only instance', :js do before do allow(Gitlab::Database).to receive(:read_only?).and_return(true) end @@ -337,7 +339,7 @@ it_behaves_like 'compares branches' end - context 'on a read-write instance' do + context 'on a read-write instance', :js do it_behaves_like 'compares branches' end end @@ -364,7 +366,9 @@ def search_for_branch(name) end def delete_branch_and_confirm - find('.js-delete-branch-button', match: :first).click + wait_for_requests + find('[data-testid="branch-more-actions"] .gl-new-dropdown-toggle', match: :first).click + find('[data-testid="delete-branch-button"]').click within '.modal-footer' do click_button 'Yes, delete branch' diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 346268af97284697689acdbc0fa78e2fac5cf2b7..0f9039019844153a4cf921f0728e178b822b8d84 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -479,7 +479,10 @@ def auto_stop_button_selector visit project_branches_filtered_path(project, state: 'all', search: 'feature') remove_branch_with_hooks(project, user, 'feature') do - page.within('.js-branch-feature') { find('.js-delete-branch-button').click } + page.within('.js-branch-feature') do + find('[data-testid="branch-more-actions"] .gl-new-dropdown-toggle').click + find('[data-testid="delete-branch-button"]').click + end end visit_environment(environment) diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb index e5c927af96c1d48f69545355885c0986e4ec5be9..6d6d850342ad7b678096c0b849080a1a11daa8aa 100644 --- a/spec/features/projects/merge_request_button_spec.rb +++ b/spec/features/projects/merge_request_button_spec.rb @@ -50,10 +50,17 @@ end it 'does not show Create merge request button' do + href = project_new_merge_request_path( + project, + merge_request: { + source_branch: 'feature' + }.merge(extra_mr_params) + ) + visit url within('#content-body') do - expect(page).not_to have_link(label) + expect(page).not_to have_link(label, href: href) end end end @@ -105,7 +112,7 @@ context 'on branches page' do it_behaves_like 'Merge request button only shown when allowed' do - let(:label) { 'Merge request' } + let(:label) { 'New' } let(:url) { project_branches_filtered_path(project, state: 'all', search: 'feature') } let(:fork_url) { project_branches_filtered_path(forked_project, state: 'all', search: 'feature') } end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index e4a64d391b0e217972d775e9e2290458fc5c873f..9244cafbc0b77ddd91b451abd8b19d309eb356a5 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -27,7 +27,7 @@ find('input[data-testid="branch-search"]').set('fix') find('input[data-testid="branch-search"]').native.send_keys(:enter) - expect(page).to have_button('Only a project maintainer or owner can delete a protected branch', disabled: true) + expect(page).not_to have_button('Delete protected branch') end end end @@ -64,9 +64,11 @@ expect(page).to have_content('fix') expect(find('.all-branches')).to have_selector('li', count: 1) + find('[data-testid="branch-more-actions"] button').click + wait_for_requests expect(page).to have_button('Delete protected branch', disabled: false) - page.find('.js-delete-branch-button').click + find('[data-testid="delete-branch-button"]').click fill_in 'delete_branch_input', with: 'fix' click_button 'Yes, delete protected branch' diff --git a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap index 300b6f4a39aa47c96250b2cd3408e071ea564cf2..0254f81482773793f57869da33230473c6366c0f 100644 --- a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap +++ b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap @@ -4,7 +4,7 @@ exports[`Delete merged branches component Delete merged branches confirmation mo + + + + + + + + Delete merged branches + + + + { + let wrapper; + let eventHubSpy; + + const findCompareButton = () => wrapper.findByTestId('compare-branch-button'); + const findDeleteButton = () => wrapper.findByTestId('delete-branch-button'); + + const createComponent = (props = {}) => { + wrapper = mountExtended(BranchMoreDropdown, { + propsData: { + branchName: 'test', + defaultBranchName: 'main', + canDeleteBranch: true, + isProtectedBranch: false, + merged: false, + comparePath: '/path/to/branch', + deletePath: '/path/to/branch', + ...props, + }, + }); + }; + + beforeEach(() => { + eventHubSpy = jest.spyOn(eventHub, '$emit'); + }); + + it('renders the compare action', () => { + createComponent(); + + expect(findCompareButton().exists()).toBe(true); + expect(findCompareButton().text()).toBe('Compare'); + }); + + it('renders the delete action', () => { + createComponent(); + + expect(findDeleteButton().exists()).toBe(true); + expect(findDeleteButton().text()).toBe('Delete branch'); + }); + + it('renders a different text for a protected branch', () => { + createComponent({ isProtectedBranch: true }); + + expect(findDeleteButton().text()).toBe('Delete protected branch'); + }); + + it('emits the data to eventHub when button is clicked', async () => { + createComponent({ merged: true }); + + await findDeleteButton().trigger('click'); + + expect(eventHubSpy).toHaveBeenCalledWith('openModal', { + branchName: 'test', + defaultBranchName: 'main', + deletePath: '/path/to/branch', + isProtectedBranch: false, + merged: true, + }); + }); + + it('doesn`t render the delete action when user cannot delete branch', () => { + createComponent({ canDeleteBranch: false }); + + expect(findDeleteButton().exists()).toBe(false); + }); +}); diff --git a/spec/frontend/branches/components/delete_branch_button_spec.js b/spec/frontend/branches/components/delete_branch_button_spec.js deleted file mode 100644 index 5b2ec443c59dc65d991a2d2f29a3d49b4d4b037a..0000000000000000000000000000000000000000 --- a/spec/frontend/branches/components/delete_branch_button_spec.js +++ /dev/null @@ -1,92 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import DeleteBranchButton from '~/branches/components/delete_branch_button.vue'; -import eventHub from '~/branches/event_hub'; - -let wrapper; -let findDeleteButton; - -const createComponent = (props = {}) => { - wrapper = shallowMount(DeleteBranchButton, { - propsData: { - branchName: 'test', - deletePath: '/path/to/branch', - defaultBranchName: 'main', - ...props, - }, - }); -}; - -describe('Delete branch button', () => { - let eventHubSpy; - - beforeEach(() => { - findDeleteButton = () => wrapper.findComponent(GlButton); - eventHubSpy = jest.spyOn(eventHub, '$emit'); - }); - - it('renders the button with default tooltip, style, and icon', () => { - createComponent(); - - expect(findDeleteButton().attributes()).toMatchObject({ - title: 'Delete branch', - variant: 'default', - icon: 'remove', - }); - }); - - it('renders a different tooltip for a protected branch', () => { - createComponent({ isProtectedBranch: true }); - - expect(findDeleteButton().attributes()).toMatchObject({ - title: 'Delete protected branch', - variant: 'default', - icon: 'remove', - }); - }); - - it('renders a different protected tooltip when it is both protected and disabled', () => { - createComponent({ isProtectedBranch: true, disabled: true }); - - expect(findDeleteButton().attributes()).toMatchObject({ - title: 'Only a project maintainer or owner can delete a protected branch', - variant: 'default', - }); - }); - - it('emits the data to eventHub when button is clicked', () => { - createComponent({ merged: true }); - - findDeleteButton().vm.$emit('click'); - - expect(eventHubSpy).toHaveBeenCalledWith('openModal', { - branchName: 'test', - defaultBranchName: 'main', - deletePath: '/path/to/branch', - isProtectedBranch: false, - merged: true, - }); - }); - - describe('#disabled', () => { - it('does not disable the button by default when mounted', () => { - createComponent(); - - expect(findDeleteButton().attributes()).toMatchObject({ - title: 'Delete branch', - variant: 'default', - }); - }); - - // Used for unallowed users and for the default branch. - it('disables the button when mounted for a disabled modal', () => { - createComponent({ disabled: true, tooltip: 'The default branch cannot be deleted' }); - - expect(findDeleteButton().attributes()).toMatchObject({ - title: 'The default branch cannot be deleted', - disabled: 'true', - variant: 'default', - }); - }); - }); -}); diff --git a/spec/helpers/branches_helper_spec.rb b/spec/helpers/branches_helper_spec.rb index 2ad15adff59225ca2d8d76a83ad4c48aec16a3c2..3375686765382eec80782b16ad2e2d522661cbfc 100644 --- a/spec/helpers/branches_helper_spec.rb +++ b/spec/helpers/branches_helper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BranchesHelper do +RSpec.describe BranchesHelper, feature_category: :source_code_management do describe '#access_levels_data' do subject { helper.access_levels_data(access_levels) } @@ -47,4 +47,53 @@ end end end + + describe '#merge_request_status' do + subject { helper.merge_request_status(merge_request) } + + let(:merge_request) { build(:merge_request, title: title) } + let(:title) { 'Test MR' } + + context 'when merge request is missing' do + let(:merge_request) { nil } + + it { is_expected.to be_nil } + end + + context 'when merge request is closed' do + before do + merge_request.close + end + + it { is_expected.to be_nil } + end + + context 'when merge request is open' do + it { is_expected.to eq(icon: 'merge-request-open', title: "Open - #{title}", variant: :success) } + end + + context 'when merge request is locked' do + let(:merge_request) { build(:merge_request, :locked, title: title) } + + it { is_expected.to eq(icon: 'merge-request-open', title: "Open - #{title}", variant: :success) } + end + + context 'when merge request is draft' do + let(:title) { 'Draft: Test MR' } + + it { is_expected.to eq(icon: 'merge-request-open', title: "Open - #{title}", variant: :warning) } + end + + context 'when merge request is merged' do + let(:merge_request) { build(:merge_request, :merged, title: title) } + + it { is_expected.to eq(icon: 'merge', title: "Merged - #{title}", variant: :info) } + end + + context 'when merge request status is unsupported' do + let(:merge_request) { build(:merge_request, state_id: -1) } + + it { is_expected.to be_nil } + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a5e68829c5d830d33235eb0ee3f4b49340b36fb2..f0ea8db9e81dcb649a3821bb973b80b0d02be8d9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -180,6 +180,26 @@ end end + describe '.by_sorted_source_branches' do + let(:fork_for_project) { fork_project(project) } + + let!(:merge_request_to_master) { create(:merge_request, :closed, target_project: project, source_branch: 'a-feature') } + let!(:merge_request_to_other_branch) { create(:merge_request, target_project: project, source_branch: 'b-feature') } + let!(:merge_request_to_master2) { create(:merge_request, target_project: project, source_branch: 'a-feature') } + let!(:merge_request_from_fork_to_master) { create(:merge_request, source_project: fork_for_project, target_project: project, source_branch: 'b-feature') } + + it 'returns merge requests sorted by name and id' do + expect(described_class.by_sorted_source_branches(%w[a-feature b-feature non-existing-feature])).to eq( + [ + merge_request_to_master2, + merge_request_to_master, + merge_request_from_fork_to_master, + merge_request_to_other_branch + ] + ) + end + end + describe '.without_hidden', feature_category: :insider_threat do let_it_be(:banned_user) { create(:user, :banned) } let_it_be(:hidden_merge_request) { create(:merge_request, :unique_branches, author: banned_user) } diff --git a/spec/views/projects/branches/index.html.haml_spec.rb b/spec/views/projects/branches/index.html.haml_spec.rb index 9954d9ecaec598b273e94c7251eec8fcc9887d0e..b2b96866904bd3165a33d2cc79a104aa0a0c9158 100644 --- a/spec/views/projects/branches/index.html.haml_spec.rb +++ b/spec/views/projects/branches/index.html.haml_spec.rb @@ -16,6 +16,7 @@ assign(:mode, 'overview') assign(:active_branches, [active_branch]) assign(:stale_branches, [stale_branch]) + assign(:related_merge_requests, {}) assign(:overview_max_branches, 5) assign(:branch_pipeline_statuses, {}) assign(:refs_pipelines, {})