diff options
27 files changed, 165 insertions, 89 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ea273334d4c..080d8cd6c7f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -211,7 +211,7 @@ rake ee_compat_check: - ee_compat_check/repo/ - vendor/ruby artifacts: - name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_REF}" + name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}" when: on_failure expire_in: 10d paths: @@ -330,7 +330,7 @@ migration paths: - sed -i 's/localhost/redis/g' config/resque.yml - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3 - bundle exec rake db:drop db:create db:schema:load db:seed_fu - - git checkout $CI_COMMIT_REF + - git checkout $CI_COMMIT_SHA - source scripts/prepare_build.sh - bundle exec rake db:migrate @@ -394,7 +394,6 @@ trigger_docs: - master@gitlab-org/gitlab-ce # Notify slack in the end - notify:slack: stage: post-test <<: *dedicated-runner @@ -402,7 +401,7 @@ notify:slack: SETUP_DB: "false" USE_BUNDLE_INSTALL: "false" script: - - ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_REF"/pipelines>" + - ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>" when: on_failure only: - master@gitlab-org/gitlab-ce diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index e48d7196c7b..a5657fc8720 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -159,7 +159,7 @@ class FilteredSearchVisualTokens { const name = token.querySelector('.name'); const value = token.querySelector('.value'); - if (token.classList.contains('filtered-search-token')) { + if (token.classList.contains('filtered-search-token') && value) { FilteredSearchVisualTokens.addFilterVisualToken(name.innerText); input.value = value.innerText; } else { diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index 863a766a255..6461eeac11c 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -8,9 +8,12 @@ class Projects::BlameController < Projects::ApplicationController def show @blob = @repository.blob_at(@commit.id, @path) - + return render_404 unless @blob + environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } + @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last + @blame_groups = Gitlab::Blame.new(@blob, @commit).groups end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 22714d9c5a4..840405f38cb 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -11,16 +11,18 @@ class Projects::BranchesController < Projects::ApplicationController @sort = params[:sort].presence || sort_value_name @branches = BranchesFinder.new(@repository, params).execute - @branches = Kaminari.paginate_array(@branches).page(params[:page]) unless params[:show_all].present? - respond_to do |format| format.html do + paginate_branches + @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) + @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 end end format.json do + paginate_branches unless params[:show_all] render json: @branches.map(&:name) end end @@ -91,6 +93,10 @@ class Projects::BranchesController < Projects::ApplicationController end end + def paginate_branches + @branches = Kaminari.paginate_array(@branches).page(params[:page]) + end + def url_to_autodeploy_setup(project, branch_name) namespace_project_new_blob_path( project.namespace, diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index ea7e4d9f663..e13f0bde315 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -14,7 +14,9 @@ class Projects::TagsController < Projects::ApplicationController @tags = TagsFinder.new(@repository, params).execute @tags = Kaminari.paginate_array(@tags).page(params[:page]) - @releases = project.releases.where(tag: @tags.map(&:name)) + tag_names = @tags.map(&:name) + @tags_pipelines = @project.pipelines.latest_successful_for_refs(tag_names) + @releases = project.releases.where(tag: tag_names) end def show diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 7f32c1b5300..0b0c6a07efd 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -203,4 +203,18 @@ module BlobHelper 'blob-language' => @blob && @blob.language.try(:ace_mode) } end + + def copy_file_path_button(file_path) + clipboard_button(clipboard_text: file_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') + end + + def copy_blob_content_button(blob) + return if markup?(blob.name) + + clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard") + end + + def open_raw_file_button(path) + link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', title: 'Open raw', data: { container: 'body' } + end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 65d08a22b4c..d1009f88549 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -115,6 +115,12 @@ module Ci success.latest(ref).order(id: :desc).first end + def self.latest_successful_for_refs(refs) + success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash| + hash[pipeline.ref] ||= pipeline + end + end + def self.truncate_sha(sha) sha[0...8] end diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 8a40281e28c..4ad77b6266d 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -7,13 +7,8 @@ #blob-content-holder.tree-holder .file-holder - .js-file-title.file-title - = blob_icon @blob.mode, @blob.name - %strong - = @path - %small= number_to_human_size @blob.size - .file-actions - = render "projects/blob/actions" + = render "projects/blob/header", blob: @blob + .table-responsive.file-content.blame.code.js-syntax-highlight %table - current_line = 1 diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml deleted file mode 100644 index 14d42f7d9ec..00000000000 --- a/app/views/projects/blob/_actions.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -- if @environment - .btn-group< - = view_on_environment_button(@commit.sha, @path, @environment) - -.btn-group{ role: "group" }< - = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), - class: 'btn btn-sm', target: '_blank' - -# only show normal/blame view links for text files - - if blob_text_viewable?(@blob) - - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) - = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), - class: 'btn btn-sm' - - else - = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id), - class: 'btn btn-sm js-blob-blame-link' unless @blob.empty? - = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), - class: 'btn btn-sm' - = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, - tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url' - -- if current_user - .btn-group{ role: "group" }< - - if blob_text_viewable?(@blob) - = edit_blob_link - = replace_blob_link - = delete_blob_link diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index bf8801bb1e3..2b2ee6ed987 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -24,13 +24,5 @@ #blob-content-holder.blob-content-holder %article.file-holder - .js-file-title.file-title-flex-parent - .file-header-content - = blob_icon blob.mode, blob.name - %strong.file-title-name - = blob.name - %small - = number_to_human_size(blob_size(blob)) - .file-actions.hidden-xs - = render "actions" + = render "projects/blob/header", blob: blob = render blob.to_partial_path(@project), blob: blob diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml new file mode 100644 index 00000000000..deeeae3d64a --- /dev/null +++ b/app/views/projects/blob/_header.html.haml @@ -0,0 +1,39 @@ +.js-file-title.file-title-flex-parent + .file-header-content + = blob_icon blob.mode, blob.name + + %strong.file-title-name + = blob.name + + = copy_file_path_button(blob.path) + + %small + = number_to_human_size(blob_size(blob)) + + .file-actions.hidden-xs + .btn-group{ role: "group" }< + = copy_blob_content_button(blob) if blob_text_viewable?(blob) + = open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id)) + = view_on_environment_button(@commit.sha, @path, @environment) if @environment + + .btn-group{ role: "group" }< + -# only show normal/blame view links for text files + - if blob_text_viewable?(blob) + - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) + = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), + class: 'btn btn-sm' + - else + = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id), + class: 'btn btn-sm js-blob-blame-link' unless blob.empty? + + = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), + class: 'btn btn-sm' + + = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, + tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url' + + - if current_user + .btn-group{ role: "group" }< + = edit_blob_link if blob_text_viewable?(blob) + = replace_blob_link + = delete_blob_link diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml index 58524418a67..b1e1be49de9 100644 --- a/app/views/projects/blob/_text.html.haml +++ b/app/views/projects/blob/_text.html.haml @@ -8,12 +8,12 @@ - else - blob.load_all_data!(@repository) - - if markup?(blob.name) - .file-content.wiki - = render_markup(blob.name, blob.data) + - if blob.empty? + .file-content.code + .nothing-here-block Empty file - else - - if blob.empty? - .file-content.code - .nothing-here-block Empty file + - if markup?(blob.name) + .file-content.wiki + = render_markup(blob.name, blob.data) - else = render 'shared/file_highlight', blob: blob, repository: @repository diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index ae63f8184df..9eb610ba9c0 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,7 +27,7 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", method: :post, title: "Compare" do Compare - = render 'projects/buttons/download', project: @project, ref: branch.name + = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name] - if can?(current_user, :push_code, @project) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index b560ed21f1d..d90d4a27cd6 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,3 +1,5 @@ +- pipeline = local_assigns.fetch(:pipeline) { project.pipelines.latest_successful_for(ref) } + - if !project.empty_repo? && can?(current_user, :download_code, project) .project-action-button.dropdown.inline> %button.btn{ 'data-toggle' => 'dropdown' } @@ -24,7 +26,6 @@ %i.fa.fa-download %span Download tar - - pipeline = project.pipelines.latest_successful_for(ref) - if pipeline - artifacts = pipeline.builds.latest.with_artifacts - if artifacts.any? diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index f809c52c367..7d6b3701f95 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -2,8 +2,11 @@ - if defined?(blob) && blob && diff_file.submodule? %span = icon('archive fw') - %span + + %strong.file-title-name = submodule_link(blob, diff_commit.id, project.repository) + + = copy_file_path_button(blob.path) - else = conditional_link_to url.present?, url do = blob_icon diff_file.b_mode, diff_file.file_path @@ -21,7 +24,7 @@ - if diff_file.deleted_file deleted - = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') + = copy_file_path_button(diff_file.new_path) - if diff_file.mode_changed? %small diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 6b3d7d4008b..e35385f4cab 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -4,13 +4,7 @@ .project-snippets %article.file-holder.snippet-file-content - .js-file-title.file-title - = blob_icon 0, @snippet.file_name - = @snippet.file_name - .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") - = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - = render 'shared/snippets/blob' + = render 'shared/snippets/blob', raw_path: raw_namespace_project_snippet_path(@project.namespace, @project, @snippet) .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 8ef069b9e05..dffe908e85a 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -23,7 +23,7 @@ = markdown_field(release, :description) .row-fixed-content.controls - = render 'projects/buttons/download', project: @project, ref: tag.name + = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name] - if can?(current_user, :push_code, @project) = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index ad5c0c2d8c8..74f71e6cbd1 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,7 +1,25 @@ -- unless @snippet.content.empty? +.js-file-title.file-title-flex-parent + .file-header-content + = blob_icon @snippet.mode, @snippet.path + + %strong.file-title-name + = @snippet.path + + = copy_file_path_button(@snippet.path) + + .file-actions.hidden-xs + .btn-group{ role: "group" }< + = copy_blob_content_button(@snippet) + = open_raw_file_button(raw_path) + + - if defined?(download_path) && download_path + = link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' } + +- if @snippet.content.empty? + .file-content.code + .nothing-here-block Empty file +- else - if markup?(@snippet.file_name) - %textarea.markdown-snippet-copy.blob-content{ data: { blob_id: @snippet.id } } - = @snippet.content .file-content.wiki - if gitlab_markdown?(@snippet.file_name) = preserve(markdown_field(@snippet, :content)) @@ -9,6 +27,3 @@ = render_markup(@snippet.file_name, @snippet.content) - else = render 'shared/file_highlight', blob: @snippet -- else - .file-content.code - .nothing-here-block Empty file diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 970afbe6b64..da9fb755a36 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -3,13 +3,7 @@ = render 'shared/snippets/header' %article.file-holder.snippet-file-content - .js-file-title.file-title - = blob_icon 0, @snippet.file_name - = @snippet.file_name - .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") - = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" - = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm" - = render 'shared/snippets/blob' + = render 'shared/snippets/blob', raw_path: raw_snippet_path(@snippet), download_path: download_snippet_path(@snippet) -= render 'award_emoji/awards_block', awardable: @snippet, inline: true +.row-content-block.top-block.content-component-block + = render 'award_emoji/awards_block', awardable: @snippet, inline: true diff --git a/changelogs/unreleased/add-blob-copy-button.yml b/changelogs/unreleased/add-blob-copy-button.yml new file mode 100644 index 00000000000..946723e523b --- /dev/null +++ b/changelogs/unreleased/add-blob-copy-button.yml @@ -0,0 +1,4 @@ +--- +title: Add copy button to blob header and use icon for Raw button +merge_request: +author: diff --git a/changelogs/unreleased/fix-slow-queries-for-branches-index.yml b/changelogs/unreleased/fix-slow-queries-for-branches-index.yml new file mode 100644 index 00000000000..f5bd7003615 --- /dev/null +++ b/changelogs/unreleased/fix-slow-queries-for-branches-index.yml @@ -0,0 +1,4 @@ +--- +title: Fixes n+1 query for tags and branches index page +merge_request: 9905 +author: diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 5baff9cc6ba..4e9094cb0f1 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -236,8 +236,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach ++ CI=true ++ export CI_DEBUG_TRACE=false ++ CI_DEBUG_TRACE=false -++ export CI_COMMIT_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045 -++ CI_COMMIT_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045 +++ export CI_COMMIT_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045 +++ CI_COMMIT_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045 ++ export CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045 ++ CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045 ++ export CI_COMMIT_REF_NAME=master diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 6845f75f22f..5c47eaf0279 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -48,7 +48,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I click link "Raw"' do - click_link 'Raw' + click_link 'Open raw' end step 'I should see raw file content' do @@ -356,7 +356,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I should see buttons for allowed commands' do page.within '.content' do - expect(page).to have_content 'Raw' + expect(page).to have_link 'Open raw' expect(page).to have_content 'History' expect(page).to have_content 'Permalink' expect(page).not_to have_content 'Edit' diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index d26a0caf036..8e0306ce83b 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -17,6 +17,14 @@ describe 'Branches', feature: true do repository.branches { |branch| expect(page).to have_content("#{branch.name}") } expect(page).to have_content("Protected branches can be managed in project settings") end + + it 'avoids a N+1 query in branches index' do + control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_branches_path(project.namespace, project) }.count + + %w(one two three four five).each { |ref| repository.add_branch(@user, ref, 'master') } + + expect { visit namespace_project_branches_path(project.namespace, project) }.not_to exceed_query_limit(control_count) + end end describe 'Find branches' do diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb index 29d2c244720..555f84c4772 100644 --- a/spec/features/tags/master_views_tags_spec.rb +++ b/spec/features/tags/master_views_tags_spec.rb @@ -27,10 +27,20 @@ feature 'Master views tags', feature: true do context 'when project has tags' do let(:project) { create(:project, namespace: user.namespace) } + let(:repository) { project.repository } + before do visit namespace_project_tags_path(project.namespace, project) end + scenario 'avoids a N+1 query in branches index' do + control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_tags_path(project.namespace, project) }.count + + %w(one two three four five).each { |tag| repository.add_tag(user, tag, 'master', 'foo') } + + expect { visit namespace_project_tags_path(project.namespace, project) }.not_to exceed_query_limit(control_count) + end + scenario 'views the tags list page' do expect(page).to have_content 'v1.0.0' end diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index fe7f3d2e9c4..549c7af8ea8 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -17,7 +17,7 @@ describe('Build', () => { spyOn($, 'ajax'); }); - describe('constructor', () => { + describe('class constructor', () => { beforeEach(() => { jasmine.clock().install(); }); diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 4a664e4fae2..53282b999dc 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -532,6 +532,19 @@ describe Ci::Pipeline, models: true do end end + describe '.latest_successful_for_refs' do + include_context 'with some outdated pipelines' + + let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') } + let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') } + + it 'returns the latest successful pipeline for both refs' do + refs = %w(ref1 ref2 ref3) + + expect(described_class.latest_successful_for_refs(refs)).to eq({ 'ref1' => latest_successful_pipeline1, 'ref2' => latest_successful_pipeline2 }) + end + end + describe '#status' do let(:build) do create(:ci_build, :created, pipeline: pipeline, name: 'test') |