From fa8052a47b713999220c8377059462a869884da8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 9 Jan 2017 10:18:47 +0100 Subject: Do not show artifacts keep button if not allowed --- app/views/projects/builds/_sidebar.html.haml | 2 +- spec/features/projects/builds_spec.rb | 28 +++++++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 0b3adcbe121..bb279c97261 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -29,7 +29,7 @@ - if @build.artifacts? .btn-group.btn-group-justified{ role: :group } - - if @build.artifacts_expire_at + - if @build.artifacts_expire_at && can?(current_user, :update_build, @build) = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do Keep diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index 8c4d4320dc5..984918e22f7 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -3,6 +3,7 @@ require 'tempfile' feature 'Builds', :feature do let(:user) { create(:user) } + let(:user_access_level) { :developer } let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } @@ -14,7 +15,7 @@ feature 'Builds', :feature do end before do - project.team << [user, :developer] + project.team << [user, user_access_level] login_as(user) end @@ -131,7 +132,9 @@ feature 'Builds', :feature do context 'Artifacts expire date' do before do - build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at) + build.update_attributes(artifacts_file: artifacts_file, + artifacts_expire_at: expire_at) + visit namespace_project_build_path(project.namespace, project, build) end @@ -146,12 +149,23 @@ feature 'Builds', :feature do context 'when expire date is defined' do let(:expire_at) { Time.now + 7.days } - it 'keeps artifacts when Keep button is clicked' do - expect(page).to have_content 'The artifacts will be removed' - click_link 'Keep' + context 'when user has ability to update build' do + it 'keeps artifacts when keep button is clicked' do + expect(page).to have_content 'The artifacts will be removed' - expect(page).not_to have_link 'Keep' - expect(page).not_to have_content 'The artifacts will be removed' + click_link 'Keep' + + expect(page).not_to have_link 'Keep' + expect(page).not_to have_content 'The artifacts will be removed' + end + end + + context 'when user does not have ability to update build' do + let(:user_access_level) { :guest } + + it 'does not have keep button' do + expect(page).to have_no_link 'Keep' + end end end -- cgit v1.2.1 From 4a7e1423f00adb7f11efc9d8342ae8236de56228 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 9 Jan 2017 10:24:09 +0100 Subject: Add changelog for artifacts button visibility fix --- changelogs/unreleased/fix-keep-artifacts-button-visibility.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-keep-artifacts-button-visibility.yml diff --git a/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml b/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml new file mode 100644 index 00000000000..3d8cf1c74a2 --- /dev/null +++ b/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml @@ -0,0 +1,4 @@ +--- +title: Hide build artifacts keep button if operation is not allowed +merge_request: 8501 +author: -- cgit v1.2.1 From 5456859b5b28baca95ced74179a349563498a5f0 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 9 Jan 2017 10:58:45 +0100 Subject: Add method that checks for expiring build artifacts --- app/models/ci/build.rb | 4 +++ app/views/projects/builds/_sidebar.html.haml | 4 +-- spec/features/projects/builds_spec.rb | 4 +-- spec/models/build_spec.rb | 45 ++++++++++++++++++++-------- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 27042798741..48ffe40abc6 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -507,6 +507,10 @@ module Ci end end + def has_expiring_artifacts? + artifacts_expire_at.present? + end + def keep_artifacts! self.update(artifacts_expire_at: nil) end diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index bb279c97261..37bf085130a 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -22,14 +22,14 @@ %p.build-detail-row The artifacts were removed #{time_ago_with_tooltip(@build.artifacts_expire_at)} - - elsif @build.artifacts_expire_at + - elsif @build.has_expiring_artifacts? %p.build-detail-row The artifacts will be removed in %span.js-artifacts-remove= @build.artifacts_expire_at - if @build.artifacts? .btn-group.btn-group-justified{ role: :group } - - if @build.artifacts_expire_at && can?(current_user, :update_build, @build) + - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build) = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do Keep diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index 984918e22f7..11d27feab0b 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -155,8 +155,8 @@ feature 'Builds', :feature do click_link 'Keep' - expect(page).not_to have_link 'Keep' - expect(page).not_to have_content 'The artifacts will be removed' + expect(page).to have_no_link 'Keep' + expect(page).to have_no_content 'The artifacts will be removed' end end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index cd3b6d51545..4d71c20f525 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -652,6 +652,24 @@ describe Ci::Build, models: true do end end + describe '#has_expiring_artifacts?' do + context 'when artifacts have expiration date set' do + before { build.update(artifacts_expire_at: 1.day.from_now) } + + it 'has expiring artifacts' do + expect(build).to have_expiring_artifacts + end + end + + context 'when artifacts do not have expiration date set' do + before { build.update(artifacts_expire_at: nil) } + + it 'does not have expiring artifacts' do + expect(build).not_to have_expiring_artifacts + end + end + end + describe '#artifacts_metadata?' do subject { build.artifacts_metadata? } context 'artifacts metadata does not exist' do @@ -663,19 +681,6 @@ describe Ci::Build, models: true do it { is_expected.to be_truthy } end end - describe '#repo_url' do - let(:build) { create(:ci_build) } - let(:project) { build.project } - - subject { build.repo_url } - - it { is_expected.to be_a(String) } - it { is_expected.to end_with(".git") } - it { is_expected.to start_with(project.web_url[0..6]) } - it { is_expected.to include(build.token) } - it { is_expected.to include('gitlab-ci-token') } - it { is_expected.to include(project.web_url[7..-1]) } - end describe '#artifacts_expire_in' do subject { build.artifacts_expire_in } @@ -721,6 +726,20 @@ describe Ci::Build, models: true do end end + describe '#repo_url' do + let(:build) { create(:ci_build) } + let(:project) { build.project } + + subject { build.repo_url } + + it { is_expected.to be_a(String) } + it { is_expected.to end_with(".git") } + it { is_expected.to start_with(project.web_url[0..6]) } + it { is_expected.to include(build.token) } + it { is_expected.to include('gitlab-ci-token') } + it { is_expected.to include(project.web_url[7..-1]) } + end + describe '#depends_on_builds' do let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } -- cgit v1.2.1 From b11492c2e36803cf19b175e7d850a72f4e5f9b1f Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Tue, 10 Jan 2017 13:30:28 -0500 Subject: Use cached values to compute total issues count in milestone index pages Milestoneish#issues_visible_to_user caches only the ActiveRecord relation, not the actual result. Introduce Milestoneish#total_issues_count that relies on the cached method Milestoneish#count_issues_by_state to reduce the number of queries. --- app/models/concerns/milestoneish.rb | 7 +++++-- app/views/shared/milestones/_milestone.html.haml | 2 +- changelogs/unreleased/reduce-queries-milestone-index.yml | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/reduce-queries-milestone-index.yml diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index fcc8feddb39..e9450dd0c26 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -7,11 +7,14 @@ module Milestoneish def total_items_count(user) memoize_per_user(user, :total_items_count) do - issues_count = count_issues_by_state(user).values.sum - issues_count + merge_requests.size + total_issues_count(user) + merge_requests.size end end + def total_issues_count(user) + count_issues_by_state(user).values.sum + end + def complete?(user) total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user) end diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 3200aacf542..9e6a76e1ddb 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -9,7 +9,7 @@ .pull-right.light #{milestone.percent_complete(current_user)}% complete .row .col-sm-6 - = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path + = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path · = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path .col-sm-6= milestone_progress_bar(milestone) diff --git a/changelogs/unreleased/reduce-queries-milestone-index.yml b/changelogs/unreleased/reduce-queries-milestone-index.yml new file mode 100644 index 00000000000..a779b58c973 --- /dev/null +++ b/changelogs/unreleased/reduce-queries-milestone-index.yml @@ -0,0 +1,4 @@ +--- +title: Use cached values to compute total issues count in milestone index pages +merge_request: 8518 +author: -- cgit v1.2.1 From 5dc8fe31ba19f321041d2ad5c73765062d41a312 Mon Sep 17 00:00:00 2001 From: Martin Cabrera Date: Tue, 10 Jan 2017 23:11:36 +0100 Subject: Fix Compare page throws 500 error when any branch/reference is not selected --- app/controllers/projects/compare_controller.rb | 7 ++++++- changelogs/unreleased/i--25814-500-error.yml | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/i--25814-500-error.yml diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index ec02fc15d35..5f14581ac69 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -25,8 +25,13 @@ class Projects::CompareController < Projects::ApplicationController end def create - redirect_to namespace_project_compare_path(@project.namespace, @project, + if params[:from].blank? || params[:to].blank? + flash[:alert] = "You must select from & to branches" + redirect_to namespace_project_compare_index_path + else + redirect_to namespace_project_compare_path(@project.namespace, @project, params[:from], params[:to]) + end end private diff --git a/changelogs/unreleased/i--25814-500-error.yml b/changelogs/unreleased/i--25814-500-error.yml new file mode 100644 index 00000000000..cd55ede84c8 --- /dev/null +++ b/changelogs/unreleased/i--25814-500-error.yml @@ -0,0 +1,4 @@ +--- +title: Fix Compare page throws 500 error when any branch/reference is not selected +merge_request: 8492 +author: Martin Cabrera -- cgit v1.2.1 From 6972a35fec05d08f2d243622d68d064105026398 Mon Sep 17 00:00:00 2001 From: Martin Cabrera Date: Tue, 10 Jan 2017 23:46:32 +0100 Subject: Added specs for branches compare when from & to are empty --- spec/controllers/projects/compare_controller_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 7a57801c437..c4905937878 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -64,6 +64,16 @@ describe Projects::CompareController do expect(assigns(:diffs)).to eq(nil) expect(assigns(:commits)).to eq(nil) end + + it 'redirects back to index when params[:from] & params[:to] are empty' do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: '', + to: '') + + expect(response).to redirect_to(namespace_project_compare_index_path) + end end describe 'GET diff_for_path' do -- cgit v1.2.1 From 7ab3dd4b302a85c1b005e9ef290ebac631cda673 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Thu, 24 Nov 2016 15:05:15 +0100 Subject: support `/merge` slash comand for MRs --- app/controllers/projects/notes_controller.rb | 3 +- app/models/merge_request.rb | 14 +++- app/services/merge_requests/update_service.rb | 15 ++++ app/services/notes/create_service.rb | 5 +- app/services/notes/slash_commands_service.rb | 4 +- app/services/slash_commands/interpret_service.rb | 16 +++- app/views/projects/notes/_form.html.haml | 1 + .../unreleased/24915_merge_slash_command.yml | 4 + doc/user/project/slash_commands.md | 1 + spec/controllers/projects/notes_controller_spec.rb | 48 +++++++++++ .../user_uses_slash_commands_spec.rb | 57 +++++++++++++ spec/models/merge_request_spec.rb | 96 ++++++++++++++++++++++ .../services/merge_requests/update_service_spec.rb | 93 +++++++++++++++++++++ spec/services/notes/create_service_spec.rb | 11 +++ .../slash_commands/interpret_service_spec.rb | 69 +++++++++++++++- 15 files changed, 429 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/24915_merge_slash_command.yml diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index b71509f2c9b..c5d93ce25bc 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -23,7 +23,8 @@ class Projects::NotesController < Projects::ApplicationController end def create - @note = Notes::CreateService.new(project, current_user, note_params).execute + create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha]) + @note = Notes::CreateService.new(project, current_user, create_params).execute if @note.is_a?(Note) Banzai::NoteRenderer.render([@note], @project, current_user) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 70005a87f4b..10251302db8 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -898,10 +898,22 @@ class MergeRequest < ActiveRecord::Base end def has_commits? - commits_count > 0 + merge_request_diff && commits_count > 0 end def has_no_commits? !has_commits? end + + def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil) + return false unless can_be_merged_by?(current_user) + + return true if autocomplete_precheck + + return false unless mergeable?(skip_ci_check: true) + return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?) + return false if last_diff_sha != diff_head_sha + + true + end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index ad16ef8c70f..3cb9aae83f6 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -7,6 +7,8 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) + merge_from_slash_command(merge_request) if params[:merge] + if merge_request.closed_without_fork? params.except!(:target_branch, :force_remove_source_branch) end @@ -69,6 +71,19 @@ module MergeRequests end end + def merge_from_slash_command(merge_request) + last_diff_sha = params.delete(:merge) + return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha) + + merge_request.update(merge_error: nil) + + if merge_request.head_pipeline && merge_request.head_pipeline.active? + MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request) + else + MergeWorker.perform_async(merge_request.id, current_user.id, {}) + end + end + def reopen_service MergeRequests::ReopenService end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 1beca9f4109..cdd765c85eb 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -1,6 +1,8 @@ module Notes class CreateService < BaseService def execute + merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha) + note = project.notes.new(params) note.author = current_user note.system = false @@ -19,7 +21,8 @@ module Notes slash_commands_service = SlashCommandsService.new(project, current_user) if slash_commands_service.supported?(note) - content, command_params = slash_commands_service.extract_commands(note) + options = { merge_request_diff_head_sha: merge_request_diff_head_sha } + content, command_params = slash_commands_service.extract_commands(note, options) only_commands = content.empty? diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb index 2edbd39a9e7..aaea9717fc4 100644 --- a/app/services/notes/slash_commands_service.rb +++ b/app/services/notes/slash_commands_service.rb @@ -19,10 +19,10 @@ module Notes self.class.supported?(note, current_user) end - def extract_commands(note) + def extract_commands(note, options = {}) return [note.note, {}] unless supported?(note) - SlashCommands::InterpretService.new(project, current_user). + SlashCommands::InterpretService.new(project, current_user, options). execute(note.note, note.noteable) end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index d75c5b1800e..14fad3ba120 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -2,7 +2,7 @@ module SlashCommands class InterpretService < BaseService include Gitlab::SlashCommands::Dsl - attr_reader :issuable + attr_reader :issuable, :options # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and hash of changes to be applied to a record. @@ -13,7 +13,8 @@ module SlashCommands opts = { issuable: issuable, current_user: current_user, - project: project + project: project, + params: params } content, commands = extractor.extract_commands(content, opts) @@ -58,6 +59,17 @@ module SlashCommands @updates[:state_event] = 'reopen' end + desc 'Merge (when build succeeds)' + condition do + last_diff_sha = params.to_h[:merge_request_diff_head_sha] + issuable.is_a?(MergeRequest) && + issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) && + issuable.persisted? + end + command :merge do + @updates[:merge] = params[:merge_request_diff_head_sha] + end + desc 'Change title' params '' condition do diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 39731668a61..b561052e721 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -3,6 +3,7 @@ = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type + = hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha) = note_target_fields(@note) = f.hidden_field :commit_id = f.hidden_field :line_code diff --git a/changelogs/unreleased/24915_merge_slash_command.yml b/changelogs/unreleased/24915_merge_slash_command.yml new file mode 100644 index 00000000000..eb8ced8ab01 --- /dev/null +++ b/changelogs/unreleased/24915_merge_slash_command.yml @@ -0,0 +1,4 @@ +--- +title: Support slash comand `/merge` for merging merge requests. +merge_request: 7746 +author: Jarka Kadlecova diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md index 5f6a6c6503e..0a66e9a3e15 100644 --- a/doc/user/project/slash_commands.md +++ b/doc/user/project/slash_commands.md @@ -14,6 +14,7 @@ do. |:---------------------------|:-------------| | `/close` | Close the issue or merge request | | `/reopen` | Reopen the issue or merge request | +| `/merge` | Merge (when build succeeds) | | `/title ` | Change title | | `/assign @username` | Assign | | `/unassign` | Remove assignee | diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 92e38b02615..9f6d4ec6537 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -14,6 +14,54 @@ describe Projects::NotesController do } end + describe 'POST create' do + let(:merge_request) { create(:merge_request) } + let(:request_params) do + { + note: { note: 'some note', noteable_id: merge_request.id, noteable_type: 'MergeRequest' }, + namespace_id: project.namespace, + project_id: project, + merge_request_diff_head_sha: 'sha' + } + end + + before do + sign_in(user) + project.team << [user, :developer] + end + + it "returns status 302 for html" do + post :create, request_params + + expect(response).to have_http_status(302) + end + + it "returns status 200 for json" do + post :create, request_params.merge(format: :json) + + expect(response).to have_http_status(200) + end + + context 'when merge_request_diff_head_sha present' do + before do + service_params = { + note: 'some note', + noteable_id: merge_request.id.to_s, + noteable_type: 'MergeRequest', + merge_request_diff_head_sha: 'sha' + } + + expect(Notes::CreateService).to receive(:new).with(project, user, service_params).and_return(double(execute: true)) + end + + it "returns status 302 for html" do + post :create, request_params + + expect(response).to have_http_status(302) + end + end + end + describe 'POST toggle_award_emoji' do before do sign_in(user) diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index b1b3a47a1ce..d7e8723b63a 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -68,6 +68,63 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do end end + describe 'merging the MR from the note' do + context 'when the current user can merge the MR' do + it 'merges the MR' do + write_note("/merge") + + expect(page).to have_content 'Your commands have been executed!' + + expect(merge_request.reload).to be_merged + end + end + + context 'when the head diff changes in the meanwhile' do + before do + path = File.expand_path("#{project.repository_storage_path}/#{project.namespace.path}/#{project.path}/new_file.txt") + + params = { + source_project: merge_request.project, + target_project: merge_request.project, + target_branch: merge_request.source_branch, + source_branch: merge_request.source_branch, + file_path: path, + file_content: 'some content', + commit_message: 'additional commit', + } + + Files::UpdateService.new(project, user, params).execute + merge_request.reload_diff + end + + it 'does not merge the MR' do + write_note("/merge") + + expect(page).not_to have_content 'Your commands have been executed!' + + expect(merge_request.reload).not_to be_merged + end + end + + context 'when the current user cannot merge the MR' do + let(:guest) { create(:user) } + before do + project.team << [guest, :guest] + logout + login_with(guest) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'does not merge the MR' do + write_note("/merge") + + expect(page).not_to have_content 'Your commands have been executed!' + + expect(merge_request.reload).not_to be_merged + end + end + end + describe 'adding a due date from note' do it 'does not recognize the command nor create a note' do write_note('/due 2016-08-28') diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 8d1385016fd..722987a423c 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1514,6 +1514,102 @@ describe MergeRequest, models: true do end end + describe '#mergeable_with_slash_command?' do + def create_pipeline(status) + create(:ci_pipeline_with_one_job, + project: project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha, + status: status) + end + + let(:project) { create(:project, :public, only_allow_merge_if_build_succeeds: true) } + let(:developer) { create(:user) } + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:mr_sha) { merge_request.diff_head_sha } + + before do + project.team << [developer, :developer] + end + + context 'when autocomplete_precheck is set to true' do + it 'is mergeable by developer' do + expect(merge_request.mergeable_with_slash_command?(developer, autocomplete_precheck: true)).to be_truthy + end + + it 'is not mergeable by normal user' do + expect(merge_request.mergeable_with_slash_command?(user, autocomplete_precheck: true)).to be_falsey + end + end + + context 'when autocomplete_precheck is set to false' do + it 'is mergeable by developer' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + end + + it 'is not mergeable by normal user' do + expect(merge_request.mergeable_with_slash_command?(user, last_diff_sha: mr_sha)).to be_falsey + end + + context 'closed MR' do + before do + merge_request.update_attribute(:state, :closed) + end + + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + end + end + + context 'MR with WIP' do + before do + merge_request.update_attribute(:title, 'WIP: some MR') + end + + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + end + end + + context 'sha differs from the MR diff_head_sha' do + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: 'some other sha')).to be_falsey + end + end + + context 'with pipeline ok' do + before do + create_pipeline(:success) + end + + it 'is mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + end + end + + context 'with failing pipeline' do + before do + create_pipeline(:failed) + end + + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + end + end + + context 'with running pipeline' do + before do + create_pipeline(:running) + end + + it 'is mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + end + end + end + end + describe '#has_commits?' do before do allow(subject.merge_request_diff).to receive(:commits_count). diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 88c786947d3..c2f6ae29d62 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -121,6 +121,99 @@ describe MergeRequests::UpdateService, services: true do end end + context 'merge' do + let(:opts) do + { + merge: merge_request.diff_head_sha + } + end + + let(:service) { MergeRequests::UpdateService.new(project, user, opts) } + + context 'without pipeline' do + before do + merge_request.merge_error = 'Error' + + perform_enqueued_jobs do + service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request).to be_valid } + it { expect(@merge_request.state).to eq('merged') } + it { expect(@merge_request.merge_error).to be_nil } + end + + context 'with finished pipeline' do + before do + create(:ci_pipeline_with_one_job, + project: project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha, + status: :success) + + perform_enqueued_jobs do + @merge_request = service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request).to be_valid } + it { expect(@merge_request.state).to eq('merged') } + end + + context 'with active pipeline' do + before do + service_mock = double + create(:ci_pipeline_with_one_job, + project: project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha) + + expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user). + and_return(service_mock) + expect(service_mock).to receive(:execute).with(merge_request) + end + + it { service.execute(merge_request) } + end + + context 'MR can not be merged by non authorised user' do + let(:visitor) { create(:user) } + let(:service) { MergeRequests::UpdateService.new(project, visitor, opts) } + + before do + merge_request.update_attribute(:merge_error, 'Error') + + perform_enqueued_jobs do + @merge_request = service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request.state).to eq('opened') } + it { expect(@merge_request.merge_error).not_to be_nil } + end + + context 'MR can not be merged when note sha != MR sha' do + let(:opts) do + { + merge: 'other_commit' + } + end + + before do + perform_enqueued_jobs do + @merge_request = service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request.state).to eq('opened') } + end + end + context 'todos' do let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 25804696d2e..b0cc3ce5f5a 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -63,6 +63,17 @@ describe Notes::CreateService, services: true do expect(note.note).to eq "HELLO\nWORLD" end end + + describe '/merge with sha option' do + let(:note_text) { %(HELLO\n/merge\nWORLD) } + let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') } + + it 'saves the note and exectues merge command' do + note = described_class.new(project, user, params).execute + + expect(note.note).to eq "HELLO\nWORLD" + end + end end end diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index becf627a4f5..dfcffcc6131 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -1,12 +1,13 @@ require 'spec_helper' describe SlashCommands::InterpretService, services: true do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } let(:developer) { create(:user) } let(:issue) { create(:issue, project: project) } let(:milestone) { create(:milestone, project: project, title: '9.10') } let(:inprogress) { create(:label, project: project, title: 'In Progress') } let(:bug) { create(:label, project: project, title: 'Bug') } + let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } before do project.team << [developer, :developer] @@ -218,6 +219,14 @@ describe SlashCommands::InterpretService, services: true do end end + shared_examples 'merge command' do + it 'runs merge command if content contains /merge' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(merge: merge_request.diff_head_sha) + end + end + it_behaves_like 'reopen command' do let(:content) { '/reopen' } let(:issuable) { issue } @@ -238,6 +247,64 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { merge_request } end + context 'merge command' do + let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: merge_request.diff_head_sha }) } + + it_behaves_like 'merge command' do + let(:content) { '/merge' } + let(:issuable) { merge_request } + end + + context 'can not be merged when logged user does not have permissions' do + let(:service) { described_class.new(project, create(:user)) } + + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { merge_request } + end + end + + context 'can not be merged when sha does not match' do + let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: 'othersha' }) } + + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { merge_request } + end + end + + context 'when sha is missing' do + let(:service) { described_class.new(project, developer, {}) } + + it 'precheck passes and returns merge command' do + _, updates = service.execute('/merge', merge_request) + + expect(updates).to eq(merge: nil) + end + end + + context 'non merge request object cant be merged' do + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { issue } + end + end + + context 'non persisted merge request cant be merged' do + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { build(:merge_request) } + end + end + + context 'not persisted merge request can not be merged' do + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { build(:merge_request, source_project: project) } + end + end + end + it_behaves_like 'title command' do let(:content) { '/title A brand new title' } let(:issuable) { issue } -- cgit v1.2.1 From aa934c7469372cac7b8cd10b49761d90d8e367fa Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Wed, 21 Dec 2016 11:32:37 +0100 Subject: refresh merge widget after using /merge command --- app/assets/javascripts/notes.js | 12 +++++ .../projects/merge_requests_controller.rb | 10 +++++ app/views/projects/merge_requests/_show.html.haml | 2 + config/routes/project.rb | 1 + .../projects/merge_requests_controller_spec.rb | 52 ++++++++++++++++++++++ 5 files changed, 77 insertions(+) diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 603db88567d..0016070b648 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -3,6 +3,7 @@ /* global GLForm */ /* global Autosave */ /* global ResolveService */ +/* global mrRefreshWidgetUrl */ /*= require autosave */ /*= require autosize */ @@ -244,6 +245,16 @@ }; + Notes.prototype.handleCreateChanges = function(note) { + if (typeof note === 'undefined') { + return; + } + + if (note.commands_changes && note.commands_changes.includes('merge')) { + $.get(mrRefreshWidgetUrl); + } + }; + /* Render note in main comments area. @@ -429,6 +440,7 @@ */ Notes.prototype.addNote = function(xhr, note, status) { + this.handleCreateChanges(note); return this.renderNote(note); }; diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 6004e7d7115..72dcf020c9f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -347,6 +347,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def merge_widget_refresh + if merge_request.in_progress_merge_commit_sha + @status = :success + elsif merge_request.merge_when_build_succeeds + @status = :merge_when_build_succeeds + end + + render 'merge' + end + def branch_from # This is always source @source_project = @merge_request.nil? ? @project : @merge_request.source_project diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 1f63803c24e..2e7cd52df1e 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -112,3 +112,5 @@ merge_request = new MergeRequest({ action: "#{controller.action_name}" }); + + var mrRefreshWidgetUrl = "#{@merge_request && @merge_request.source_project ? merge_widget_refresh_namespace_project_merge_request_url(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request) : ''}"; diff --git a/config/routes/project.rb b/config/routes/project.rb index 26e2dc9e6e7..1fc6ed28c74 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -94,6 +94,7 @@ constraints(ProjectUrlConstrainer.new) do get :pipelines get :merge_check post :merge + get :merge_widget_refresh post :cancel_merge_when_build_succeeds get :ci_status get :ci_environments_status diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 2a411d78395..c6a38863cf5 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1048,4 +1048,56 @@ describe Projects::MergeRequestsController do end end end + + describe 'GET merge_widget_refresh' do + let(:params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + format: :raw + } + end + + before do + project.team << [user, :developer] + xhr :get, :merge_widget_refresh, params + end + + context 'when merge in progress' do + let(:merge_request) { create(:merge_request, source_project: project, in_progress_merge_commit_sha: 'sha') } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'returns :success' do + expect(assigns(:status)).to eq(:success) + end + end + + context 'when waiting for build' do + let(:merge_request) { create(:merge_request, source_project: project, merge_when_build_succeeds: true, merge_user: user) } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'returns :merge_when_build_succeeds' do + expect(assigns(:status)).to eq(:merge_when_build_succeeds) + end + end + + context 'when no special status for MR' do + let(:merge_request) { create(:merge_request, source_project: project) } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'returns nil' do + expect(assigns(:status)).to be_nil + end + end + end end -- cgit v1.2.1 From ca0cf5a3cd2829db4cfac007c36d5588ed369f87 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 30 Dec 2016 17:16:25 -0200 Subject: Show 'too many changes' message for merge request --- app/helpers/diff_helper.rb | 6 +++++ app/models/merge_request_diff.rb | 20 ++++++++--------- app/views/devise/shared/_signup_box.html.haml | 2 +- app/views/projects/diffs/_diffs.html.haml | 4 ++-- .../projects/merge_requests/show/_diffs.html.haml | 10 +-------- changelogs/unreleased/issue_25017.yml | 4 ++++ spec/features/merge_requests/diffs_spec.rb | 14 ++++++++++++ spec/models/merge_request_diff_spec.rb | 26 ++++++++++++++++++++++ 8 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 changelogs/unreleased/issue_25017.yml diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index c35d6611ab0..aed1d7c839f 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -165,4 +165,10 @@ module DiffHelper link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class] end + + def render_overflow_warning?(diff_files) + diffs = @merge_request_diff.presence || diff_files + + diffs.overflow? + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index b8f36a2c958..f0e2fadc32b 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -234,28 +234,28 @@ class MergeRequestDiff < ActiveRecord::Base # and save it as array of hashes in st_diffs db field def save_diffs new_attributes = {} - new_diffs = [] if commits.size.zero? new_attributes[:state] = :empty else diff_collection = compare.diffs(Commit.max_diff_options) - - if diff_collection.overflow? - # Set our state to 'overflow' to make the #empty? and #collected? - # methods (generated by StateMachine) return false. - new_attributes[:state] = :overflow - end - - new_attributes[:real_size] = diff_collection.real_size + new_attributes[:real_size] = compare.diffs.real_size if diff_collection.any? new_diffs = dump_diffs(diff_collection) new_attributes[:state] = :collected end + + new_attributes[:st_diffs] = new_diffs || [] + + # Set our state to 'overflow' to make the #empty? and #collected? + # methods (generated by StateMachine) return false. + # + # This attribution has to come at the end of the method so 'overflow' + # state does not get overridden by 'collected'. + new_attributes[:state] = :overflow if diff_collection.overflow? end - new_attributes[:st_diffs] = new_diffs update_columns_serialized(new_attributes) end diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 681eb303b49..01ecf237925 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -15,7 +15,7 @@ .form-group = f.label :email = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address." - %div.form-group + .form-group = f.label :email_confirmation = f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address." .form-group.append-bottom-20#password-strength diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index ab4a2dc36e5..58c20e225c6 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -18,8 +18,8 @@ = parallel_diff_btn = render 'projects/diffs/stats', diff_files: diff_files -- if diff_files.overflow? - = render 'projects/diffs/warning', diff_files: diff_files +- if render_overflow_warning?(diff_files) + = render 'projects/diffs/warning', diff_files: diffs .files{ data: { can_create_note: can_create_note } } - diff_files.each_with_index do |diff_file| diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 99c71e1454a..5f048d04b27 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,13 +1,5 @@ -- if @merge_request_diff.collected? +- if @merge_request_diff.collected? || @merge_request_diff.overflow? = render 'projects/merge_requests/show/versions' = render "projects/diffs/diffs", diffs: @diffs - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} -- else - .alert.alert-warning - %h4 - Changes view for this comparison is extremely large. - %p - You can - = link_to "download it", merge_request_path(@merge_request, format: :diff), class: "vlink" - instead. diff --git a/changelogs/unreleased/issue_25017.yml b/changelogs/unreleased/issue_25017.yml new file mode 100644 index 00000000000..09126ae81bc --- /dev/null +++ b/changelogs/unreleased/issue_25017.yml @@ -0,0 +1,4 @@ +--- +title: Show 'too many changes' message for created merge requests when they are too large +merge_request: +author: diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index c9a0059645d..4a6c76a5caf 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -22,4 +22,18 @@ feature 'Diffs URL', js: true, feature: true do expect(page).to have_css('.diffs.tab-pane.active') end end + + context 'when merge request has overflow' do + it 'displays warning' do + allow_any_instance_of(MergeRequestDiff).to receive(:overflow?).and_return(true) + allow(Commit).to receive(:max_diff_options).and_return(max_files: 20, max_lines: 20) + + visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + + page.within('.alert') do + expect(page).to have_text("Too many changes to show. Plain diff Email patch To preserve + performance only 3 of 3+ files are displayed.") + end + end + end end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index eb876d105da..6d599e148a2 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -76,6 +76,32 @@ describe MergeRequestDiff, models: true do end end + describe '#save_diffs' do + it 'saves collected state' do + mr_diff = create(:merge_request).merge_request_diff + + expect(mr_diff.collected?).to be_truthy + end + + it 'saves overflow state' do + allow(Commit).to receive(:max_diff_options) + .and_return(max_lines: 0, max_files: 0) + + mr_diff = create(:merge_request).merge_request_diff + + expect(mr_diff.overflow?).to be_truthy + end + + it 'saves empty state' do + allow_any_instance_of(MergeRequestDiff).to receive(:commits) + .and_return([]) + + mr_diff = create(:merge_request).merge_request_diff + + expect(mr_diff.empty?).to be_truthy + end + end + describe '#commits_sha' do it 'returns all commits SHA using serialized commits' do subject.st_commits = [ -- cgit v1.2.1 From 557a0bf14c79c02c65196ff8f7a2251ecd77073c Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Fri, 30 Dec 2016 20:49:59 +0100 Subject: Address MR comments --- app/assets/javascripts/notes.js | 2 +- .../projects/merge_requests_controller.rb | 2 +- app/helpers/merge_requests_helper.rb | 8 ++++++++ app/services/slash_commands/interpret_service.rb | 6 +++--- app/views/projects/merge_requests/_show.html.haml | 3 ++- .../projects/merge_requests_controller_spec.rb | 22 +++++++++++++++++++--- .../user_uses_slash_commands_spec.rb | 18 +++--------------- spec/helpers/merge_requests_helper_spec.rb | 15 +++++++++++++++ spec/models/merge_request_spec.rb | 6 ++++++ .../services/merge_requests/update_service_spec.rb | 2 +- .../slash_commands/interpret_service_spec.rb | 2 +- 11 files changed, 60 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 0016070b648..fac21f8cd32 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -250,7 +250,7 @@ return; } - if (note.commands_changes && note.commands_changes.includes('merge')) { + if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) { $.get(mrRefreshWidgetUrl); } }; diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 72dcf020c9f..6d6115413a5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -348,7 +348,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def merge_widget_refresh - if merge_request.in_progress_merge_commit_sha + if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged' @status = :success elsif merge_request.merge_when_build_succeeds @status = :merge_when_build_succeeds diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 20218775659..8c2c4e8833b 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -19,6 +19,14 @@ module MergeRequestsHelper } end + def mr_widget_refresh_url(mr) + if mr && mr.source_project + merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr) + else + '' + end + end + def mr_css_classes(mr) classes = "merge-request" classes << " closed" if mr.closed? diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 14fad3ba120..d18844ac0e3 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -61,10 +61,10 @@ module SlashCommands desc 'Merge (when build succeeds)' condition do - last_diff_sha = params.to_h[:merge_request_diff_head_sha] + last_diff_sha = params && params[:merge_request_diff_head_sha] issuable.is_a?(MergeRequest) && - issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) && - issuable.persisted? + issuable.persisted? && + issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) end command :merge do @updates[:merge] = params[:merge_request_diff_head_sha] diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 2e7cd52df1e..d95017286ba 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -113,4 +113,5 @@ action: "#{controller.action_name}" }); - var mrRefreshWidgetUrl = "#{@merge_request && @merge_request.source_project ? merge_widget_refresh_namespace_project_merge_request_url(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request) : ''}"; + var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}"; + diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c6a38863cf5..7ea3ea4f376 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1071,8 +1071,22 @@ describe Projects::MergeRequestsController do expect(response).to have_http_status(:ok) end - it 'returns :success' do + it 'sets status to :success' do + expect(assigns(:status)).to eq(:success) + expect(response).to render_template('merge') + end + end + + context 'when merge request was merged already' do + let(:merge_request) { create(:merge_request, source_project: project, state: :merged) } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'sets status to :success' do expect(assigns(:status)).to eq(:success) + expect(response).to render_template('merge') end end @@ -1083,8 +1097,9 @@ describe Projects::MergeRequestsController do expect(response).to have_http_status(:ok) end - it 'returns :merge_when_build_succeeds' do + it 'sets status to :merge_when_build_succeeds' do expect(assigns(:status)).to eq(:merge_when_build_succeeds) + expect(response).to render_template('merge') end end @@ -1095,8 +1110,9 @@ describe Projects::MergeRequestsController do expect(response).to have_http_status(:ok) end - it 'returns nil' do + it 'sets status to nil' do expect(assigns(:status)).to be_nil + expect(response).to render_template('merge') end end end diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index d7e8723b63a..b13674b4db9 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -73,7 +73,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do it 'merges the MR' do write_note("/merge") - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' expect(merge_request.reload).to be_merged end @@ -81,20 +81,8 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do context 'when the head diff changes in the meanwhile' do before do - path = File.expand_path("#{project.repository_storage_path}/#{project.namespace.path}/#{project.path}/new_file.txt") - - params = { - source_project: merge_request.project, - target_project: merge_request.project, - target_branch: merge_request.source_branch, - source_branch: merge_request.source_branch, - file_path: path, - file_content: 'some content', - commit_message: 'additional commit', - } - - Files::UpdateService.new(project, user, params).execute - merge_request.reload_diff + merge_request.source_branch = 'another_branch' + merge_request.save end it 'does not merge the MR' do diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 903224589dd..1f221487393 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -62,4 +62,19 @@ describe MergeRequestsHelper do it { is_expected.to eq([source_title, target_title]) } end end + + describe 'mr_widget_refresh_url' do + let(:merge_request) { create(:merge_request, source_project: project) } + let(:project) { create(:project) } + + it 'returns correct url for MR' do + expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh" + + expect(mr_widget_refresh_url(merge_request)).to end_with(expected_url) + end + + it 'returns empty string for nil' do + expect(mr_widget_refresh_url(nil)).to end_with('') + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 722987a423c..861426acbc3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1578,6 +1578,12 @@ describe MergeRequest, models: true do end end + context 'sha is not provided' do + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer)).to be_falsey + end + end + context 'with pipeline ok' do before do create_pipeline(:success) diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index c2f6ae29d62..7d73c0ea5d0 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -179,7 +179,7 @@ describe MergeRequests::UpdateService, services: true do it { service.execute(merge_request) } end - context 'MR can not be merged by non authorised user' do + context 'with a non-authorised user' do let(:visitor) { create(:user) } let(:service) { MergeRequests::UpdateService.new(project, visitor, opts) } diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index dfcffcc6131..ffcf02d2c56 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -283,7 +283,7 @@ describe SlashCommands::InterpretService, services: true do end end - context 'non merge request object cant be merged' do + context 'issue can not be merged' do it_behaves_like 'empty command' do let(:content) { "/merge" } let(:issuable) { issue } -- cgit v1.2.1 From c2283a2db716cf52818a99ac8db1ddb7839818d7 Mon Sep 17 00:00:00 2001 From: Martin Cabrera Date: Sun, 15 Jan 2017 01:12:19 +0100 Subject: Changed alert message character: from & to and --- app/controllers/projects/compare_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 5f14581ac69..746ca6568f8 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -26,7 +26,8 @@ class Projects::CompareController < Projects::ApplicationController def create if params[:from].blank? || params[:to].blank? - flash[:alert] = "You must select from & to branches" + flash[:alert] = "You must select from and to branches" + byebug redirect_to namespace_project_compare_index_path else redirect_to namespace_project_compare_path(@project.namespace, @project, -- cgit v1.2.1 From 0413a7ad03b94171038336ca7e13c7911e3ea3d3 Mon Sep 17 00:00:00 2001 From: Martin Cabrera Date: Sun, 15 Jan 2017 01:53:56 +0100 Subject: from or to get variables gets preserved if the other one is missing --- app/controllers/projects/compare_controller.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 746ca6568f8..91793cc3650 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -27,8 +27,8 @@ class Projects::CompareController < Projects::ApplicationController def create if params[:from].blank? || params[:to].blank? flash[:alert] = "You must select from and to branches" - byebug - redirect_to namespace_project_compare_index_path + from_to_preservation = from_to_hash(params) + redirect_to namespace_project_compare_index_path(@project.namespace, @project, from_to_preservation) else redirect_to namespace_project_compare_path(@project.namespace, @project, params[:from], params[:to]) @@ -62,4 +62,11 @@ class Projects::CompareController < Projects::ApplicationController @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) end + + def from_to_hash(params) + return_hash = {} + return_hash[:from] = params[:from].presence + return_hash[:to] = params[:to].presence + return_hash + end end -- cgit v1.2.1 From 87c39b6d393d211c038bff084a3f8d21f0391d71 Mon Sep 17 00:00:00 2001 From: Martin Cabrera Date: Sun, 15 Jan 2017 02:28:01 +0100 Subject: Added specs for the preservation of the from/to parameter --- .../projects/compare_controller_spec.rb | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index c4905937878..b03c4b52de6 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -65,7 +65,27 @@ describe Projects::CompareController do expect(assigns(:commits)).to eq(nil) end - it 'redirects back to index when params[:from] & params[:to] are empty' do + it 'redirects back to index when params[:from] is empty and preserves params[:to]' do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: '', + to: 'master') + + expect(response).to redirect_to(namespace_project_compare_index_path(project.namespace, project, to: 'master')) + end + + it 'redirects back to index when params[:to] is empty and preserves params[:from]' do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: 'master', + to: '') + + expect(response).to redirect_to(namespace_project_compare_index_path(project.namespace, project, from: 'master')) + end + + it 'redirects back to index when params[:from] and params[:to] are empty' do post(:create, namespace_id: project.namespace.to_param, project_id: project.to_param, -- cgit v1.2.1 From 64dd41a0e21360c380cab394f8a5c9b4945b7fd1 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Tue, 20 Dec 2016 15:44:24 +0100 Subject: Backport timetracking frontend to CE. --- .../diff_notes/diff_notes_bundle.js.es6 | 2 - .../javascripts/issuable/issuable_bundle.js.es6 | 1 + .../components/collapsed_state.js.es6 | 42 +++++ .../components/comparison_pane.js.es6 | 69 +++++++ .../components/estimate_only_pane.js.es6 | 13 ++ .../time_tracking/components/help_state.js.es6 | 24 +++ .../components/no_tracking_pane.js.es6 | 11 ++ .../components/spent_only_pane.js.es6 | 13 ++ .../time_tracking/components/time_tracker.js.es6 | 118 ++++++++++++ .../time_tracking/time_tracking_bundle.js.es6 | 61 +++++++ app/assets/stylesheets/framework/variables.scss | 3 +- app/assets/stylesheets/pages/issuable.scss | 99 ++++++++++ app/views/projects/issues/show.html.haml | 2 + app/views/projects/merge_requests/_show.html.haml | 1 + .../projects/merge_requests/conflicts.html.haml | 1 + app/views/shared/icons/_icon_stopwatch.svg | 1 + app/views/shared/issuable/_sidebar.html.haml | 14 +- config/application.rb | 2 + .../issues/user_uses_slash_commands_spec.rb | 26 +++ spec/javascripts/issuable_time_tracker_spec.js.es6 | 201 +++++++++++++++++++++ spec/javascripts/pretty_time_spec.js.es6 | 22 +-- spec/support/time_tracking_shared_examples.rb | 82 +++++++++ 22 files changed, 792 insertions(+), 16 deletions(-) create mode 100644 app/assets/javascripts/issuable/issuable_bundle.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 create mode 100644 app/views/shared/icons/_icon_stopwatch.svg create mode 100644 spec/javascripts/issuable_time_tracker_spec.js.es6 create mode 100644 spec/support/time_tracking_shared_examples.rb diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 index 840b5aa922e..1b3a57d0962 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -2,8 +2,6 @@ /* global Vue */ /* global ResolveCount */ -//= require vue -//= require vue-resource //= require_directory ./models //= require_directory ./stores //= require_directory ./services diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js.es6 new file mode 100644 index 00000000000..7d0465aa8b4 --- /dev/null +++ b/app/assets/javascripts/issuable/issuable_bundle.js.es6 @@ -0,0 +1 @@ +//= require ./time_tracking/time_tracking_bundle diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 new file mode 100644 index 00000000000..72433df2818 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 @@ -0,0 +1,42 @@ +/* global Vue */ +//= require lib/utils/pretty_time + +(() => { + Vue.component('time-tracking-collapsed-state', { + name: 'time-tracking-collapsed-state', + props: [ + 'showComparisonState', + 'showSpentOnlyState', + 'showEstimateOnlyState', + 'showNoTimeTrackingState', + 'timeSpentHumanReadable', + 'timeEstimateHumanReadable', + 'stopwatchSvg', + ], + methods: { + abbreviateTime(timeStr) { + return gl.utils.prettyTime.abbreviateTime(timeStr); + }, + }, + template: ` + + `, + }); +})(); + diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 new file mode 100644 index 00000000000..6abbd5dd167 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 @@ -0,0 +1,69 @@ +/* global Vue */ +//= require lib/utils/pretty_time + +(() => { + const prettyTime = gl.utils.prettyTime; + + Vue.component('time-tracking-comparison-pane', { + name: 'time-tracking-comparison-pane', + props: [ + 'timeSpent', + 'timeEstimate', + 'timeSpentHumanReadable', + 'timeEstimateHumanReadable', + ], + computed: { + parsedRemaining() { + const diffSeconds = this.timeEstimate - this.timeSpent; + return prettyTime.parseSeconds(diffSeconds); + }, + timeRemainingHumanReadable() { + return prettyTime.stringifyTime(this.parsedRemaining); + }, + timeRemainingTooltip() { + const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; + return `${prefix} ${this.timeRemainingHumanReadable}`; + }, + /* Diff values for comparison meter */ + timeRemainingMinutes() { + return this.timeEstimate - this.timeSpent; + }, + timeRemainingPercent() { + return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`; + }, + timeRemainingStatusClass() { + return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; + }, + /* Parsed time values */ + parsedEstimate() { + return prettyTime.parseSeconds(this.timeEstimate); + }, + parsedSpent() { + return prettyTime.parseSeconds(this.timeSpent); + }, + }, + template: ` +
+
+
+
+
+
+
+ Spent + {{ timeSpentHumanReadable }} +
+
+ Est + {{ timeEstimateHumanReadable }} +
+
+
+
+ `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 new file mode 100644 index 00000000000..309e9f2f9ef --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 @@ -0,0 +1,13 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-estimate-only-pane', { + name: 'time-tracking-estimate-only-pane', + props: ['timeEstimateHumanReadable'], + template: ` +
+ Estimated: + {{ timeEstimateHumanReadable }} +
+ `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 new file mode 100644 index 00000000000..d7ced6d7151 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 @@ -0,0 +1,24 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-help-state', { + name: 'time-tracking-help-state', + props: ['docsUrl'], + template: ` +
+
+

Track time with slash commands

+

Slash commands can be used in the issues description and comment boxes.

+

+ /estimate + will update the estimated time with the latest command. +

+

+ /spend + will update the sum of the time spent. +

+ Learn more +
+
+ `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 new file mode 100644 index 00000000000..1d2ca643b5b --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 @@ -0,0 +1,11 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-no-tracking-pane', { + name: 'time-tracking-no-tracking-pane', + template: ` +
+ No estimate or time spent +
+ `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 new file mode 100644 index 00000000000..ed283fec3c3 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 @@ -0,0 +1,13 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-spent-only-pane', { + name: 'time-tracking-spent-only-pane', + props: ['timeSpentHumanReadable'], + template: ` +
+ Spent: + {{ timeSpentHumanReadable }} +
+ `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 new file mode 100644 index 00000000000..26563a7713b --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 @@ -0,0 +1,118 @@ +/* global Vue */ +//= require ./help_state +//= require ./collapsed_state +//= require ./spent_only_pane +//= require ./no_tracking_pane +//= require ./estimate_only_pane +//= require ./comparison_pane + +(() => { + Vue.component('issuable-time-tracker', { + name: 'issuable-time-tracker', + props: [ + 'time_estimate', + 'time_spent', + 'human_time_estimate', + 'human_time_spent', + 'stopwatchSvg', + 'docsUrl', + ], + data() { + return { + showHelp: false, + }; + }, + computed: { + timeSpent() { + return this.time_spent; + }, + timeEstimate() { + return this.time_estimate; + }, + timeEstimateHumanReadable() { + return this.human_time_estimate; + }, + timeSpentHumanReadable() { + return this.human_time_spent; + }, + hasTimeSpent() { + return !!this.timeSpent; + }, + hasTimeEstimate() { + return !!this.timeEstimate; + }, + showComparisonState() { + return this.hasTimeEstimate && this.hasTimeSpent; + }, + showEstimateOnlyState() { + return this.hasTimeEstimate && !this.hasTimeSpent; + }, + showSpentOnlyState() { + return this.hasTimeSpent && !this.hasTimeEstimate; + }, + showNoTimeTrackingState() { + return !this.hasTimeEstimate && !this.hasTimeSpent; + }, + showHelpState() { + return !!this.showHelp; + }, + }, + methods: { + toggleHelpState(show) { + this.showHelp = show; + }, + }, + template: ` +
+ + +
+ Time tracking +
+ +
+
+ +
+
+
+ + + + + + + + + + + + +
+
+ `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 new file mode 100644 index 00000000000..0b8da2b1f4f --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 @@ -0,0 +1,61 @@ +/* global Vue */ +//= require ./components/time_tracker +//= require smart_interval +//= require subbable_resource + +(() => { + /* This Vue instance represents what will become the parent instance for the + * sidebar. It will be responsible for managing `issuable` state and propagating + * changes to sidebar components. We will want to create a separate service to + * interface with the server at that point. + */ + + class IssuableTimeTracking { + constructor(issuableJSON) { + const parsedIssuable = JSON.parse(issuableJSON); + return this.initComponent(parsedIssuable); + } + + initComponent(parsedIssuable) { + this.parentInstance = new Vue({ + el: '#issuable-time-tracker', + data: { + issuable: parsedIssuable, + }, + methods: { + fetchIssuable() { + return gl.IssuableResource.get.call(gl.IssuableResource, { + type: 'GET', + url: gl.IssuableResource.endpoint, + }); + }, + updateState(data) { + this.issuable = data; + }, + subscribeToUpdates() { + gl.IssuableResource.subscribe(data => this.updateState(data)); + }, + listenForSlashCommands() { + $(document).on('ajax:success', '.gfm-form', (e, data) => { + const subscribedCommands = ['spend_time', 'time_estimate']; + const changedCommands = data.commands_changes; + + if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) { + this.fetchIssuable(); + } + }); + }, + }, + created() { + this.fetchIssuable(); + }, + mounted() { + this.subscribeToUpdates(); + this.listenForSlashCommands(); + }, + }); + } + } + + gl.IssuableTimeTracking = IssuableTimeTracking; +})(window.gl || (window.gl = {})); diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index cf9424ea5dd..4baf6ee781a 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -5,7 +5,7 @@ $sidebar_collapsed_width: 62px; $sidebar_width: 220px; $gutter_collapsed_width: 62px; $gutter_width: 290px; -$gutter_inner_width: 258px; +$gutter_inner_width: 250px; $sidebar-transition-duration: .15s; $sidebar-breakpoint: 1024px; @@ -85,6 +85,7 @@ $warning-message-border: #f0e2bb; */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; +$sidebar-collapsed-icon-color: #999; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 3272a862b85..c9014ac2906 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -469,3 +469,102 @@ } } } + +.time_tracker { + padding-bottom: 0; + border-bottom: 0; + + + .sidebar-collapsed-icon { + + > .stopwatch-svg { + display: inline-block; + } + + svg { + width: 16px; + height: 16px; + fill: $sidebar-collapsed-icon-color; + } + + &:hover svg { + fill: $gl-gray; + } + } + + .help-button, + .close-help-button { + cursor: pointer; + } + + .compare-meter { + &.within_estimate { + .meter-fill { + background: $gl-primary; + } + } + + &.over_estimate { + .meter-fill { + background: $red-light; + } + + .time-remaining, + .compare-value.spent { + color: $red-light; + } + } + } + + .meter-container { + background: $border-gray-light; + border-radius: 3px; + + .meter-fill { + max-width: 100%; + height: 5px; + border-radius: 3px; + background: $gl-primary; + } + } + + .compare-display-container { + display: flex; + justify-content: space-between; + margin-top: 5px; + + .compare-display { + font-size: 13px; + color: $gl-gray-light; + + .compare-value { + color: $gl-gray; + } + } + } + + .time-tracking-help-state { + background: $white-light; + margin: 16px -20px 0; + padding: 16px 20px; + border-top: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-light; + + a:hover { + color: $btn-white-active; + } + } + + .help-state-toggle-enter-active { + transition: all .8s ease; + } + + .help-state-toggle-leave-active { + transition: all .5s ease; + } + + .help-state-toggle-enter, + .help-state-toggle-leave-active { + opacity: 0; + } +} diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 43141971231..9fa00811af0 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -2,6 +2,8 @@ - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/vue_resource.js') .clearfix.detail-page-header .issuable-header diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 110dd11d1ce..0516801cdf3 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -3,6 +3,7 @@ - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/vue_resource.js') = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js') .merge-request{ 'data-url' => merge_request_path(@merge_request) } diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index b8b87dcdcaf..ebef2157d34 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -1,5 +1,6 @@ - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/vue_resource.js') = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js') = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/show/mr_title" diff --git a/app/views/shared/icons/_icon_stopwatch.svg b/app/views/shared/icons/_icon_stopwatch.svg new file mode 100644 index 00000000000..f20de04538e --- /dev/null +++ b/app/views/shared/icons/_icon_stopwatch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index a02b815e3cd..c46710af758 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,5 +1,7 @@ - todo = issuable_todo(issuable) -%aside.right-sidebar{ class: sidebar_gutter_collapsed_class } +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('issuable/issuable_bundle.js') +%aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header @@ -72,7 +74,13 @@ .selectbox.hide-collapsed = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) - + - if issuable.has_attribute?(:time_estimate) + #issuable-time-tracker.block + %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md')} + // Fallback while content is loading + .title.hide-collapsed + Time tracking + = icon('spinner spin') - if issuable.has_attribute?(:due_date) .block.due_date .sidebar-collapsed-icon @@ -162,6 +170,8 @@ = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left") :javascript + gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}'); + new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}"); new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); new LabelsSelect(); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); diff --git a/config/application.rb b/config/application.rb index 8ce549cebf6..f00e58a36ca 100644 --- a/config/application.rb +++ b/config/application.rb @@ -88,6 +88,7 @@ module Gitlab config.assets.precompile << "print.css" config.assets.precompile << "notify.css" config.assets.precompile << "mailers/*.css" + config.assets.precompile << "lib/vue_resource.js" config.assets.precompile << "katex.css" config.assets.precompile << "katex.js" config.assets.precompile << "xterm/xterm.css" @@ -98,6 +99,7 @@ module Gitlab config.assets.precompile << "protected_branches/protected_branches_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "merge_request_widget/ci_bundle.js" + config.assets.precompile << "issuable/issuable_bundle.js" config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 31f75512f4a..f2d4aadf540 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -100,6 +100,32 @@ feature 'Issues > User uses slash commands', feature: true, js: true do end end + describe 'Issuable time tracking' do + let(:issue) { create(:issue, project: project) } + + before do + project.team << [user, :developer] + end + + context 'Issue' do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like 'issuable time tracker' + end + + context 'Merge Request' do + let(:merge_request) { create(:merge_request, source_project: project) } + + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it_behaves_like 'issuable time tracker' + end + end + describe 'toggling the WIP prefix from the title from note' do let(:issue) { create(:issue, project: project) } diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js.es6 new file mode 100644 index 00000000000..a1e979e8d09 --- /dev/null +++ b/spec/javascripts/issuable_time_tracker_spec.js.es6 @@ -0,0 +1,201 @@ +/* eslint-disable */ +//= require jquery +//= require vue +//= require issuable/time_tracking/components/time_tracker + +function initTimeTrackingComponent(opts) { + fixture.set(` +
+
+
+ `); + + this.initialData = { + time_estimate: opts.timeEstimate, + time_spent: opts.timeSpent, + human_time_estimate: opts.timeEstimateHumanReadable, + human_time_spent: opts.timeSpentHumanReadable, + docsUrl: '/help/workflow/time_tracking.md', + }; + + const TimeTrackingComponent = Vue.component('issuable-time-tracker'); + this.timeTracker = new TimeTrackingComponent({ + el: '#mock-container', + propsData: this.initialData, + }); +} + +((gl) => { + describe('Issuable Time Tracker', function() { + describe('Initialization', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' }); + }); + + it('should return something defined', function() { + expect(this.timeTracker).toBeDefined(); + }); + + it ('should correctly set timeEstimate', function(done) { + Vue.nextTick(() => { + expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate); + done(); + }); + }); + it ('should correctly set time_spent', function(done) { + Vue.nextTick(() => { + expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent); + done(); + }); + }); + }); + + describe('Content Display', function() { + describe('Panes', function() { + describe('Comparison pane', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' }); + }); + + it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) { + Vue.nextTick(() => { + const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane'); + expect(this.timeTracker.showComparisonState).toBe(true); + done(); + }); + }); + + describe('Remaining meter', function() { + it('should display the remaining meter with the correct width', function(done) { + Vue.nextTick(() => { + const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width; + const correctWidth = '5%'; + + expect(meterWidth).toBe(correctWidth); + done(); + }) + }); + + it('should display the remaining meter with the correct background color when within estimate', function(done) { + Vue.nextTick(() => { + const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill'); + expect(styledMeter.length).toBe(1); + done() + }); + }); + + it('should display the remaining meter with the correct background color when over estimate', function(done) { + this.timeTracker.time_estimate = 100000; + this.timeTracker.time_spent = 20000000; + Vue.nextTick(() => { + const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill'); + expect(styledMeter.length).toBe(1); + done(); + }); + }); + }); + }); + + describe("Estimate only pane", function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' }); + }); + + it('should display the human readable version of time estimated', function(done) { + Vue.nextTick(() => { + const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText; + const correctText = 'Estimated: 2h 46m'; + + expect(estimateText).toBe(correctText); + done(); + }); + }); + }); + + describe('Spent only pane', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' }); + }); + + it('should display the human readable version of time spent', function(done) { + Vue.nextTick(() => { + const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText; + const correctText = 'Spent: 1h 23m'; + + expect(spentText).toBe(correctText); + done(); + }); + }); + }); + + describe('No time tracking pane', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: 0, timeSpentHumanReadable: 0 }); + }); + + it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) { + Vue.nextTick(() => { + const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane'); + const noTrackingText =$noTrackingPane.innerText; + const correctText = 'No estimate or time spent'; + + expect(this.timeTracker.showNoTimeTrackingState).toBe(true); + expect($noTrackingPane).toBeVisible(); + expect(noTrackingText).toBe(correctText); + done(); + }); + }); + }); + + describe("Help pane", function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 }); + }); + + it('should not show the "Help" pane by default', function(done) { + Vue.nextTick(() => { + const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); + + expect(this.timeTracker.showHelpState).toBe(false); + expect($helpPane).toBeNull(); + done(); + }); + }); + + it('should show the "Help" pane when help button is clicked', function(done) { + Vue.nextTick(() => { + $(this.timeTracker.$el).find('.help-button').click(); + + setTimeout(() => { + const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); + expect(this.timeTracker.showHelpState).toBe(true); + expect($helpPane).toBeVisible(); + done(); + }, 10); + }); + }); + + it('should not show the "Help" pane when help button is clicked and then closed', function(done) { + Vue.nextTick(() => { + $(this.timeTracker.$el).find('.help-button').click(); + + setTimeout(() => { + + $(this.timeTracker.$el).find('.close-help-button').click(); + + setTimeout(() => { + const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); + + expect(this.timeTracker.showHelpState).toBe(false); + expect($helpPane).toBeNull(); + + done(); + }, 1000); + }, 1000); + }); + }); + }); + }); + }); + }); +})(window.gl || (window.gl = {})); diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6 index 2e12d45f7a7..7a04fba5f7f 100644 --- a/spec/javascripts/pretty_time_spec.js.es6 +++ b/spec/javascripts/pretty_time_spec.js.es6 @@ -1,12 +1,12 @@ //= require lib/utils/pretty_time (() => { - const PrettyTime = gl.PrettyTime; + const prettyTime = gl.utils.prettyTime; - describe('PrettyTime methods', function () { + describe('prettyTime methods', function () { describe('parseSeconds', function () { it('should correctly parse a negative value', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const zeroSeconds = parser(-1000); @@ -17,7 +17,7 @@ }); it('should correctly parse a zero value', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const zeroSeconds = parser(0); @@ -28,7 +28,7 @@ }); it('should correctly parse a small non-zero second values', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const subOneMinute = parser(10); @@ -53,7 +53,7 @@ }); it('should correctly parse large second values', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const aboveOneHour = parser(4800); @@ -87,7 +87,7 @@ minutes: 20, }; - const timeString = PrettyTime.stringifyTime(timeObject); + const timeString = prettyTime.stringifyTime(timeObject); expect(timeString).toBe('1w 4d 7h 20m'); }); @@ -100,7 +100,7 @@ minutes: 20, }; - const timeString = PrettyTime.stringifyTime(timeObject); + const timeString = prettyTime.stringifyTime(timeObject); expect(timeString).toBe('4d 20m'); }); @@ -113,7 +113,7 @@ minutes: 0, }; - const timeString = PrettyTime.stringifyTime(timeObject); + const timeString = prettyTime.stringifyTime(timeObject); expect(timeString).toBe('0m'); }); @@ -122,12 +122,12 @@ describe('abbreviateTime', function () { it('should abbreviate stringified times for weeks', function () { const fullTimeString = '1w 3d 4h 5m'; - expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('1w'); + expect(prettyTime.abbreviateTime(fullTimeString)).toBe('1w'); }); it('should abbreviate stringified times for non-weeks', function () { const fullTimeString = '0w 3d 4h 5m'; - expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('3d'); + expect(prettyTime.abbreviateTime(fullTimeString)).toBe('3d'); }); }); }); diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb new file mode 100644 index 00000000000..02657684b57 --- /dev/null +++ b/spec/support/time_tracking_shared_examples.rb @@ -0,0 +1,82 @@ +shared_examples 'issuable time tracker' do + it 'renders the sidebar component empty state' do + page.within '.time-tracking-no-tracking-pane' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'updates the sidebar component when estimate is added' do + submit_time('/estimate 3w 1d 1h') + + page.within '.time-tracking-estimate-only-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'updates the sidebar component when spent is added' do + submit_time('/spend 3w 1d 1h') + + page.within '.time-tracking-spend-only-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'shows the comparison when estimate and spent are added' do + submit_time('/estimate 3w 1d 1h') + submit_time('/spend 3w 1d 1h') + + page.within '.time-tracking-comparison-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'updates the sidebar component when estimate is removed' do + submit_time('/estimate 3w 1d 1h') + submit_time('/remove_estimate') + + page.within '#issuable-time-tracker' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'updates the sidebar component when spent is removed' do + submit_time('/spend 3w 1d 1h') + submit_time('/remove_time_spent') + + page.within '#issuable-time-tracker' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'shows the help state when icon is clicked' do + page.within '#issuable-time-tracker' do + find('.help-button').click + expect(page).to have_content 'Track time with slash commands' + expect(page).to have_content 'Learn more' + end + end + + it 'hides the help state when close icon is clicked' do + page.within '#issuable-time-tracker' do + find('.help-button').click + find('.close-help-button').click + + expect(page).not_to have_content 'Track time with slash commands' + expect(page).not_to have_content 'Learn more' + end + end + + it 'displays the correct help url' do + page.within '#issuable-time-tracker' do + find('.help-button').click + + expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md') + end + end +end + +def submit_time(slash_command) + fill_in 'note[note]', with: slash_command + click_button 'Comment' + wait_for_ajax +end -- cgit v1.2.1 From 17196a2ff31c4eb65fa9ecff6f7208171e26059b Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Fri, 23 Dec 2016 00:44:02 -0500 Subject: Backport backend work for time tracking. --- app/assets/javascripts/lib/vue_resource.js.es6 | 2 + app/assets/stylesheets/framework/variables.scss | 2 + app/helpers/issuables_helper.rb | 9 +++ app/models/concerns/issuable.rb | 1 + app/models/concerns/time_trackable.rb | 58 ++++++++++++++ app/models/timelog.rb | 6 ++ app/serializers/issuable_entity.rb | 4 + app/services/issuable_base_service.rb | 26 ++++++- app/services/slash_commands/interpret_service.rb | 47 ++++++++++++ app/services/system_note_service.rb | 51 +++++++++++++ ...0161223034433_add_time_estimate_to_issuables.rb | 30 ++++++++ db/migrate/20161223034646_create_timelogs.rb | 38 +++++++++ db/schema.rb | 14 ++++ doc/user/project/slash_commands.md | 4 + doc/workflow/README.md | 1 + .../time-tracking/time-tracking-example.png | Bin 0 -> 48350 bytes .../time-tracking/time-tracking-sidebar.png | Bin 0 -> 19467 bytes doc/workflow/time_tracking.md | 76 ++++++++++++++++++ lib/gitlab/import_export/import_export.yml | 2 + lib/gitlab/time_tracking_formatter.rb | 30 ++++++++ .../controllers/projects/issues_controller_spec.rb | 48 ++++++++---- spec/factories/timelogs.rb | 9 +++ .../issues/user_uses_slash_commands_spec.rb | 26 +++++++ spec/lib/gitlab/import_export/all_models.yml | 5 ++ .../gitlab/import_export/safe_model_attributes.yml | 10 +++ spec/models/concerns/issuable_spec.rb | 38 +++++++++ spec/models/timelog_spec.rb | 10 +++ spec/services/notes/slash_commands_service_spec.rb | 12 +++ .../slash_commands/interpret_service_spec.rb | 85 +++++++++++++++++++++ spec/services/system_note_service_spec.rb | 65 ++++++++++++++++ 30 files changed, 692 insertions(+), 17 deletions(-) create mode 100644 app/assets/javascripts/lib/vue_resource.js.es6 create mode 100644 app/models/concerns/time_trackable.rb create mode 100644 app/models/timelog.rb create mode 100644 db/migrate/20161223034433_add_time_estimate_to_issuables.rb create mode 100644 db/migrate/20161223034646_create_timelogs.rb create mode 100644 doc/workflow/time-tracking/time-tracking-example.png create mode 100644 doc/workflow/time-tracking/time-tracking-sidebar.png create mode 100644 doc/workflow/time_tracking.md create mode 100644 lib/gitlab/time_tracking_formatter.rb create mode 100644 spec/factories/timelogs.rb create mode 100644 spec/models/timelog_spec.rb diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6 new file mode 100644 index 00000000000..eff1dcabfa2 --- /dev/null +++ b/app/assets/javascripts/lib/vue_resource.js.es6 @@ -0,0 +1,2 @@ +//= require vue +//= require vue-resource diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4baf6ee781a..ee1c95fd373 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -56,6 +56,7 @@ $black-transparent: rgba(0, 0, 0, 0.3); $border-white-light: darken($white-light, $darken-border-factor); $border-white-normal: darken($white-normal, $darken-border-factor); +$border-gray-light: darken($gray-light, $darken-border-factor); $border-gray-normal: darken($gray-normal, $darken-border-factor); $border-gray-dark: darken($white-normal, $darken-border-factor); @@ -274,6 +275,7 @@ $dropdown-hover-color: #3b86ff; */ $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; +$btn-white-active: #848484; /* * Badges diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 1c213983a5b..e5bb8b93e76 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -30,6 +30,15 @@ module IssuablesHelper end end + def serialize_issuable(issuable) + case issuable + when Issue + IssueSerializer.new.represent(issuable).to_json + when MergeRequest + MergeRequestSerializer.new.represent(issuable).to_json + end + end + def template_dropdown_tag(issuable, &block) title = selected_template(issuable) || "Choose a template" options = { diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5e63825bf99..3517969eabc 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -13,6 +13,7 @@ module Issuable include StripAttribute include Awardable include Taskable + include TimeTrackable included do cache_markdown_field :title, pipeline: :single_line diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb new file mode 100644 index 00000000000..6fa2af4e4e6 --- /dev/null +++ b/app/models/concerns/time_trackable.rb @@ -0,0 +1,58 @@ +# == TimeTrackable concern +# +# Contains functionality related to objects that support time tracking. +# +# Used by Issue and MergeRequest. +# + +module TimeTrackable + extend ActiveSupport::Concern + + included do + attr_reader :time_spent + + alias_method :time_spent?, :time_spent + + default_value_for :time_estimate, value: 0, allows_nil: false + + has_many :timelogs, as: :trackable, dependent: :destroy + end + + def spend_time(seconds, user) + return if seconds == 0 + + @time_spent = seconds + @time_spent_user = user + + if seconds == :reset + reset_spent_time + else + add_or_subtract_spent_time + end + end + + def total_time_spent + timelogs.sum(:time_spent) + end + + def human_total_time_spent + Gitlab::TimeTrackingFormatter.output(total_time_spent) + end + + def human_time_estimate + Gitlab::TimeTrackingFormatter.output(time_estimate) + end + + private + + def reset_spent_time + timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) + end + + def add_or_subtract_spent_time + # Exit if time to subtract exceeds the total time spent. + return if time_spent < 0 && (time_spent.abs > total_time_spent) + + timelogs.new(time_spent: time_spent, user: @time_spent_user) + end +end diff --git a/app/models/timelog.rb b/app/models/timelog.rb new file mode 100644 index 00000000000..f768c4e3da5 --- /dev/null +++ b/app/models/timelog.rb @@ -0,0 +1,6 @@ +class Timelog < ActiveRecord::Base + validates :time_spent, :user, presence: true + + belongs_to :trackable, polymorphic: true + belongs_to :user +end diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb index 17c9160cb19..29aecb50849 100644 --- a/app/serializers/issuable_entity.rb +++ b/app/serializers/issuable_entity.rb @@ -13,4 +13,8 @@ class IssuableEntity < Grape::Entity expose :created_at expose :updated_at expose :deleted_at + expose :time_estimate + expose :total_time_spent + expose :human_time_estimate + expose :human_total_time_spent end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 4ce5fd993d9..7491c256b99 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -36,6 +36,14 @@ class IssuableBaseService < BaseService end end + def create_time_estimate_note(issuable) + SystemNoteService.change_time_estimate(issuable, issuable.project, current_user) + end + + def create_time_spent_note(issuable) + SystemNoteService.change_time_spent(issuable, issuable.project, current_user) + end + def filter_params(issuable) ability_name = :"admin_#{issuable.to_ability_name}" @@ -156,6 +164,7 @@ class IssuableBaseService < BaseService def create(issuable) merge_slash_commands_into_params!(issuable) filter_params(issuable) + change_time_spent(issuable) params.delete(:state_event) params[:author] ||= current_user @@ -198,13 +207,14 @@ class IssuableBaseService < BaseService change_subscription(issuable) change_todo(issuable) filter_params(issuable) + time_spent = change_time_spent(issuable) old_labels = issuable.labels.to_a old_mentioned_users = issuable.mentioned_users.to_a label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids) - if params.present? && update_issuable(issuable, params) + if (params.present? || time_spent) && update_issuable(issuable, params) # We do not touch as it will affect a update on updated_at field ActiveRecord::Base.no_touching do handle_common_system_notes(issuable, old_labels: old_labels) @@ -251,6 +261,12 @@ class IssuableBaseService < BaseService end end + def change_time_spent(issuable) + time_spent = params.delete(:spend_time) + + issuable.spend_time(time_spent, current_user) if time_spent + end + def has_changes?(issuable, old_labels: []) valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch] @@ -272,6 +288,14 @@ class IssuableBaseService < BaseService create_task_status_note(issuable) end + if issuable.previous_changes.include?('time_estimate') + create_time_estimate_note(issuable) + end + + if issuable.time_spent? + create_time_spent_note(issuable) + end + create_labels_note(issuable, old_labels) if issuable.labels != old_labels end end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index d75c5b1800e..ea00415ae1f 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -243,6 +243,53 @@ module SlashCommands @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' end + desc 'Set time estimate' + params '<1w 3d 2h 14m>' + condition do + current_user.can?(:"admin_#{issuable.to_ability_name}", project) + end + command :estimate do |raw_duration| + time_estimate = Gitlab::TimeTrackingFormatter.parse(raw_duration) + + if time_estimate + @updates[:time_estimate] = time_estimate + end + end + + desc 'Add or substract spent time' + params '<1h 30m | -1h 30m>' + condition do + current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) + end + command :spend do |raw_duration| + reduce_time = raw_duration.sub!(/\A-/, '') + time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration) + + if time_spent + time_spent *= -1 if reduce_time + + @updates[:spend_time] = time_spent + end + end + + desc 'Remove time estimate' + condition do + issuable.persisted? && + current_user.can?(:"admin_#{issuable.to_ability_name}", project) + end + command :remove_estimate do + @updates[:time_estimate] = 0 + end + + desc 'Remove spent time' + condition do + issuable.persisted? && + current_user.can?(:"admin_#{issuable.to_ability_name}", project) + end + command :remove_time_spent do + @updates[:spend_time] = :reset + end + # This is a dummy command, so that it appears in the autocomplete commands desc 'CC' params '@user' diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 7613ecd5021..5ca2551ee61 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -109,6 +109,57 @@ module SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + # Called when the estimated time of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # time_estimate - Estimated time + # + # Example Note text: + # + # "Changed estimate of this issue to 3d 5h" + # + # Returns the created Note object + + def change_time_estimate(noteable, project, author) + parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate) + body = if noteable.time_estimate == 0 + "Removed time estimate on this #{noteable.human_class_name}" + else + "Changed time estimate of this #{noteable.human_class_name} to #{parsed_time}" + end + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the spent time of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # time_spent - Spent time + # + # Example Note text: + # + # "Added 2h 30m of time spent on this issue" + # + # Returns the created Note object + + def change_time_spent(noteable, project, author) + time_spent = noteable.time_spent + + if time_spent == :reset + body = "Removed time spent on this #{noteable.human_class_name}" + else + parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs) + action = time_spent > 0 ? 'Added' : 'Subtracted' + body = "#{action} #{parsed_time} of time spent on this #{noteable.human_class_name}" + end + + create_note(noteable: noteable, project: project, author: author, note: body) + end + # Called when the status of a Noteable is changed # # noteable - Noteable object diff --git a/db/migrate/20161223034433_add_time_estimate_to_issuables.rb b/db/migrate/20161223034433_add_time_estimate_to_issuables.rb new file mode 100644 index 00000000000..8d89756a9bc --- /dev/null +++ b/db/migrate/20161223034433_add_time_estimate_to_issuables.rb @@ -0,0 +1,30 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddTimeEstimateToIssuables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :issues, :time_estimate, :integer + add_column :merge_requests, :time_estimate, :integer + end +end diff --git a/db/migrate/20161223034646_create_timelogs.rb b/db/migrate/20161223034646_create_timelogs.rb new file mode 100644 index 00000000000..d3353a67eec --- /dev/null +++ b/db/migrate/20161223034646_create_timelogs.rb @@ -0,0 +1,38 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateTimelogs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :timelogs do |t| + t.integer :time_spent, null: false + t.references :trackable, polymorphic: true + t.references :user + + t.timestamps null: false + end + + add_index :timelogs, [:trackable_type, :trackable_id] + add_index :timelogs, :user_id + end +end diff --git a/db/schema.rb b/db/schema.rb index c58a886b0fa..7815392c1c3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -506,6 +506,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do t.integer "lock_version" t.text "title_html" t.text "description_html" + t.integer "time_estimate" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -685,6 +686,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do t.integer "lock_version" t.text "title_html" t.text "description_html" + t.integer "time_estimate" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -1128,6 +1130,18 @@ ActiveRecord::Schema.define(version: 20170106172224) do add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree + create_table "timelogs", force: :cascade do |t| + t.integer "time_spent", null: false + t.integer "trackable_id" + t.string "trackable_type" + t.integer "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree + add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree + create_table "todos", force: :cascade do |t| t.integer "user_id", null: false t.integer "project_id", null: false diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md index 5f6a6c6503e..87c9756ea5d 100644 --- a/doc/user/project/slash_commands.md +++ b/doc/user/project/slash_commands.md @@ -29,3 +29,7 @@ do. | /due <in 2 days | this Friday | December 31st> | Set due date | | `/remove_due_date` | Remove due date | | `/wip` | Toggle the Work In Progress status | +| /estimate <1w 3d 2h 14m> | Set time estimate | +| `/remove_estimate` | Remove estimated time | +| /spend <1h 30m | -1h 5m> | Add or substract spent time | +| `/remove_time_spent` | Remove time spent | diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 59a806de210..b317bd79ded 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -19,6 +19,7 @@ - [Slash commands](../user/project/slash_commands.md) - [Sharing a project with a group](share_with_group.md) - [Share projects with other groups](share_projects_with_other_groups.md) +- [Time tracking](time_tracking.md) - [Web Editor](../user/project/repository/web_editor.md) - [Releases](releases.md) - [Milestones](milestones.md) diff --git a/doc/workflow/time-tracking/time-tracking-example.png b/doc/workflow/time-tracking/time-tracking-example.png new file mode 100644 index 00000000000..bbcabb602d6 Binary files /dev/null and b/doc/workflow/time-tracking/time-tracking-example.png differ diff --git a/doc/workflow/time-tracking/time-tracking-sidebar.png b/doc/workflow/time-tracking/time-tracking-sidebar.png new file mode 100644 index 00000000000..d1ff5571f95 Binary files /dev/null and b/doc/workflow/time-tracking/time-tracking-sidebar.png differ diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md new file mode 100644 index 00000000000..3b3103110d3 --- /dev/null +++ b/doc/workflow/time_tracking.md @@ -0,0 +1,76 @@ +# Time Tracking + +> Introduced in GitLab 8.14. + +Time Tracking lets teams stack their project estimates against their time spent. + +Other interesting links: + +- [Time Tracking landing page on about.gitlab.com][landing] + +## Overview + +Time Tracking lets you: +* record the time spent working on an issue or a merge request, +* add an estimate of the amount of time needed to complete an issue or a merge +request. + +You don't have to indicate an estimate to enter the time spent, and vice versa. + +Data about time tracking is shown on the issue/merge request sidebar, as shown +below. + +![Time tracking in the sidebar](time-tracking/time-tracking-sidebar.png) + +## How to enter data + +Time Tracking uses two [slash commands] that GitLab introduced with this new +feature: `/spend` and `/estimate`. + +Slash commands can be used in the body of an issue or a merge request, but also +in a comment in both an issue or a merge request. + +Below is an example of how you can use those new slash commands inside a comment. + +![Time tracking example in a comment](time-tracking/time-tracking-example.png) + +Adding time entries (time spent or estimates) is limited to project members. + +### Estimates + +To enter an estimate, write `/estimate`, followed by the time. For example, if +you need to enter an estimate of 3 days, 5 hours and 10 minutes, you would write +`/estimate 3d 5h 10m`. + +Every time you enter a new time estimate, any previous time estimates will be +overridden by this new value. There should only be one valid estimate in an +issue or a merge request. + +To remove an estimation entirely, use `/remove_estimation`. + +### Time spent + +To enter a time spent, use `/spend 3d 5h 10m`. + +Every new time spent entry will be added to the current total time spent for the +issue or the merge request. + +You can remove time by entering a negative amount: `/spend -3d` will remove 3 +days from the total time spent. You can't go below 0 minutes of time spent, +so GitLab will automatically reset the time spent if you remove a larger amount +of time compared to the time that was entered already. + +To remove all the time spent at once, use `/remove_time_spent`. + +## Configuration + +The following time units are available: +* weeks (w) +* days (d) +* hours (h) +* minutes (m) + +Default conversion rates are 1w = 5d and 1d = 8h. + +[landing]: https://about.gitlab.com/features/time-tracking +[slash-commands]: ../user/project/slash_commands.md diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index e6ecd118609..08ad3274b38 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -6,6 +6,7 @@ project_tree: - :events - issues: - :events + - :timelogs - notes: - :author - :events @@ -27,6 +28,7 @@ project_tree: - :events - :merge_request_diff - :events + - :timelogs - label_links: - label: :priorities diff --git a/lib/gitlab/time_tracking_formatter.rb b/lib/gitlab/time_tracking_formatter.rb new file mode 100644 index 00000000000..d09063c6c8f --- /dev/null +++ b/lib/gitlab/time_tracking_formatter.rb @@ -0,0 +1,30 @@ +module Gitlab + module TimeTrackingFormatter + extend self + + def parse(string) + with_custom_config do + ChronicDuration.parse(string, default_unit: 'hours') rescue nil + end + end + + def output(seconds) + with_custom_config do + ChronicDuration.output(seconds, format: :short, limit_to_hours: false, weeks: true) rescue nil + end + end + + def with_custom_config + # We may want to configure it through project settings in a future version. + ChronicDuration.hours_per_day = 8 + ChronicDuration.days_per_week = 5 + + result = yield + + ChronicDuration.hours_per_day = 24 + ChronicDuration.days_per_week = 7 + + result + end + end +end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index e2321f2034b..b5987a83df0 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -326,6 +326,20 @@ describe Projects::IssuesController do end describe 'POST #create' do + def post_new_issue(attrs = {}) + sign_in(user) + project = create(:empty_project, :public) + project.team << [user, :developer] + + post :create, { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + issue: { title: 'Title', description: 'Description' }.merge(attrs) + } + + project.issues.first + end + context 'resolving discussions in MergeRequest' do let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } let(:merge_request) { discussion.noteable } @@ -369,13 +383,7 @@ describe Projects::IssuesController do end def post_spam_issue - sign_in(user) - spam_project = create(:empty_project, :public) - post :create, { - namespace_id: spam_project.namespace.to_param, - project_id: spam_project.to_param, - issue: { title: 'Spam Title', description: 'Spam lives here' } - } + post_new_issue(title: 'Spam Title', description: 'Spam lives here') end it 'rejects an issue recognized as spam' do @@ -396,18 +404,26 @@ describe Projects::IssuesController do request.env['action_dispatch.remote_ip'] = '127.0.0.1' end - def post_new_issue + it 'creates a user agent detail' do + expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1) + end + end + + context 'when description has slash commands' do + before do sign_in(user) - project = create(:empty_project, :public) - post :create, { - namespace_id: project.namespace.to_param, - project_id: project.to_param, - issue: { title: 'Title', description: 'Description' } - } end - it 'creates a user agent detail' do - expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1) + it 'can add spent time' do + issue = post_new_issue(description: '/spend 1h') + + expect(issue.total_time_spent).to eq(3600) + end + + it 'can set the time estimate' do + issue = post_new_issue(description: '/estimate 2h') + + expect(issue.time_estimate).to eq(7200) end end end diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb new file mode 100644 index 00000000000..12fc4ec4486 --- /dev/null +++ b/spec/factories/timelogs.rb @@ -0,0 +1,9 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :timelog do + time_spent 3600 + user + association :trackable, factory: :issue + end +end diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index f2d4aadf540..0a9cd11ad6e 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -126,6 +126,32 @@ feature 'Issues > User uses slash commands', feature: true, js: true do end end + describe 'Issuable time tracking' do + let(:issue) { create(:issue, project: project) } + + before do + project.team << [user, :developer] + end + + context 'Issue' do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like 'issuable time tracker' + end + + context 'Merge Request' do + let(:merge_request) { create(:merge_request, source_project: project) } + + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it_behaves_like 'issuable time tracker' + end + end + describe 'toggling the WIP prefix from the title from note' do let(:issue) { create(:issue, project: project) } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index ceed9c942c1..7fb6829f582 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -15,6 +15,7 @@ issues: - events - merge_requests_closing_issues - metrics +- timelogs events: - author - project @@ -77,6 +78,7 @@ merge_requests: - events - merge_requests_closing_issues - metrics +- timelogs merge_request_diff: - merge_request pipelines: @@ -198,3 +200,6 @@ award_emoji: - user priorities: - label +timelogs: +- trackable +- user diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index d88a141b458..493bc2db21a 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -20,6 +20,7 @@ Issue: - lock_version - milestone_id - weight +- time_estimate Event: - id - target_type @@ -150,6 +151,7 @@ MergeRequest: - milestone_id - approvals_before_merge - rebase_commit_sha +- time_estimate MergeRequestDiff: - id - state @@ -344,3 +346,11 @@ LabelPriority: - priority - created_at - updated_at +Timelog: +- id +- time_spent +- trackable_id +- trackable_type +- user_id +- created_at +- updated_at diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 1078c959419..344906c581b 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -408,4 +408,42 @@ describe Issue, "Issuable" do expect(issue.assignee_or_author?(user)).to eq(false) end end + + describe '#spend_time' do + let(:user) { create(:user) } + let(:issue) { create(:issue) } + + def spend_time(seconds) + issue.spend_time(seconds, user) + issue.save! + end + + context 'adding time' do + it 'should update the total time spent' do + spend_time(1800) + + expect(issue.total_time_spent).to eq(1800) + end + end + + context 'substracting time' do + before do + spend_time(1800) + end + + it 'should update the total time spent' do + spend_time(-900) + + expect(issue.total_time_spent).to eq(900) + end + + context 'when time to substract exceeds the total time spent' do + it 'should not alter the total time spent' do + spend_time(-3600) + + expect(issue.total_time_spent).to eq(1800) + end + end + end + end end diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb new file mode 100644 index 00000000000..f08935b6425 --- /dev/null +++ b/spec/models/timelog_spec.rb @@ -0,0 +1,10 @@ +require 'rails_helper' + +RSpec.describe Timelog, type: :model do + subject { build(:timelog) } + + it { is_expected.to be_valid } + + it { is_expected.to validate_presence_of(:time_spent) } + it { is_expected.to validate_presence_of(:user) } +end diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb index 960b5cd5e6f..1a64c8bbf00 100644 --- a/spec/services/notes/slash_commands_service_spec.rb +++ b/spec/services/notes/slash_commands_service_spec.rb @@ -86,6 +86,18 @@ describe Notes::SlashCommandsService, services: true do expect(note.noteable).to be_open end end + + describe '/spend' do + let(:note_text) { '/spend 1h' } + + it 'updates the spent time on the noteable' do + content, command_params = service.extract_commands(note) + service.execute(command_params, note) + + expect(content).to eq '' + expect(note.noteable.time_spent).to eq(3600) + end + end end describe 'note with command & text' do diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index becf627a4f5..e1358acd7c1 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -210,6 +210,46 @@ describe SlashCommands::InterpretService, services: true do end end + shared_examples 'estimate command' do + it 'populates time_estimate: 3600 if content contains /estimate 1h' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(time_estimate: 3600) + end + end + + shared_examples 'spend command' do + it 'populates spend_time: 3600 if content contains /spend 1h' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(spend_time: 3600) + end + end + + shared_examples 'spend command with negative time' do + it 'populates spend_time: -1800 if content contains /spend -30m' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(spend_time: -1800) + end + end + + shared_examples 'remove_estimate command' do + it 'populates time_estimate: 0 if content contains /remove_estimate' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(time_estimate: 0) + end + end + + shared_examples 'remove_time_spent command' do + it 'populates spend_time: :reset if content contains /remove_time_spent' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(spend_time: :reset) + end + end + shared_examples 'empty command' do it 'populates {} if content contains an unsupported command' do _, updates = service.execute(content, issuable) @@ -451,6 +491,51 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { merge_request } end + it_behaves_like 'estimate command' do + let(:content) { '/estimate 1h' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/estimate' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/estimate abc' } + let(:issuable) { issue } + end + + it_behaves_like 'spend command' do + let(:content) { '/spend 1h' } + let(:issuable) { issue } + end + + it_behaves_like 'spend command with negative time' do + let(:content) { '/spend -30m' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/spend' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/spend abc' } + let(:issuable) { issue } + end + + it_behaves_like 'remove_estimate command' do + let(:content) { '/remove_estimate' } + let(:issuable) { issue } + end + + it_behaves_like 'remove_time_spent command' do + let(:content) { '/remove_time_spent' } + let(:issuable) { issue } + end + context 'when current_user cannot :admin_issue' do let(:visitor) { create(:user) } let(:issue) { create(:issue, project: project, author: visitor) } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 0e8adb68721..e85545f46dc 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -740,4 +740,69 @@ describe SystemNoteService, services: true do expect(note.note).to include(issue.to_reference) end end + + describe '.change_time_estimate' do + subject { described_class.change_time_estimate(noteable, project, author) } + + it_behaves_like 'a system note' + + context 'with a time estimate' do + it 'sets the note text' do + noteable.update_attribute(:time_estimate, 277200) + + expect(subject.note).to eq "Changed time estimate of this issue to 1w 4d 5h" + end + end + + context 'without a time estimate' do + it 'sets the note text' do + expect(subject.note).to eq "Removed time estimate on this issue" + end + end + end + + describe '.change_time_spent' do + # We need a custom noteable in order to the shared examples to be green. + let(:noteable) do + mr = create(:merge_request, source_project: project) + mr.spend_time(1, author) + mr.save! + mr + end + + subject do + described_class.change_time_spent(noteable, project, author) + end + + it_behaves_like 'a system note' + + context 'when time was added' do + it 'sets the note text' do + spend_time!(277200) + + expect(subject.note).to eq "Added 1w 4d 5h of time spent on this merge request" + end + end + + context 'when time was subtracted' do + it 'sets the note text' do + spend_time!(-277200) + + expect(subject.note).to eq "Subtracted 1w 4d 5h of time spent on this merge request" + end + end + + context 'when time was removed' do + it 'sets the note text' do + spend_time!(:reset) + + expect(subject.note).to eq "Removed time spent on this merge request" + end + end + + def spend_time!(seconds) + noteable.spend_time(seconds, author) + noteable.save! + end + end end -- cgit v1.2.1 From 770a199fcfab05321da055bd7b15ce82417d8e3c Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 23 Dec 2016 01:56:31 -0500 Subject: Prevent including Vue twice --- app/assets/javascripts/subbable_resource.js.es6 | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index 932120157a3..d8191605128 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -1,6 +1,3 @@ -//= require vue -//= require vue-resource - (() => { /* * SubbableResource can be extended to provide a pubsub-style service for one-off REST -- cgit v1.2.1 From b5ab18c69dd37662f4b1f01f2bd6977995291d02 Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Fri, 23 Dec 2016 02:07:34 -0500 Subject: Bring changes that were present only in EE. --- .../javascripts/lib/utils/pretty_time.js.es6 | 26 ++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js.es6 index ccaf447eb0b..ae397212e55 100644 --- a/app/assets/javascripts/lib/utils/pretty_time.js.es6 +++ b/app/assets/javascripts/lib/utils/pretty_time.js.es6 @@ -4,13 +4,13 @@ * stringifyTime condensed or non-condensed, abbreviateTimelengths) * */ - class PrettyTime { - + const utils = window.gl.utils = gl.utils || {}; + const prettyTime = utils.prettyTime = { /* * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } * Seconds can be negative or positive, zero or non-zero. */ - static parseSeconds(seconds) { + parseSeconds(seconds) { const DAYS_PER_WEEK = 5; const HOURS_PER_DAY = 8; const MINUTES_PER_HOUR = 60; @@ -24,7 +24,7 @@ minutes: 1, }; - let unorderedMinutes = PrettyTime.secondsToMinutes(seconds); + let unorderedMinutes = prettyTime.secondsToMinutes(seconds); return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); @@ -33,35 +33,33 @@ return periodCount; }); - } + }, /* * Accepts a timeObject and returns a condensed string representation of it * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. */ - static stringifyTime(timeObject) { + stringifyTime(timeObject) { const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { const isNonZero = !!unitValue; return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; }, '').trim(); return reducedTime.length ? reducedTime : '0m'; - } + }, /* * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns * the first non-zero unit/value pair. */ - static abbreviateTime(timeStr) { + abbreviateTime(timeStr) { return timeStr.split(' ') .filter(unitStr => unitStr.charAt(0) !== '0')[0]; - } + }, - static secondsToMinutes(seconds) { + secondsToMinutes(seconds) { return Math.abs(seconds / 60); - } - } - - gl.PrettyTime = PrettyTime; + }, + }; })(window.gl || (window.gl = {})); -- cgit v1.2.1 From b55d2d6573596ae1adddc7c03aef59083e676c2e Mon Sep 17 00:00:00 2001 From: Regis Freyd Date: Tue, 3 Jan 2017 13:18:02 -0500 Subject: Slight modification to the doc --- doc/workflow/time_tracking.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md index 3b3103110d3..de12994c516 100644 --- a/doc/workflow/time_tracking.md +++ b/doc/workflow/time_tracking.md @@ -2,11 +2,8 @@ > Introduced in GitLab 8.14. -Time Tracking lets teams stack their project estimates against their time spent. - -Other interesting links: - -- [Time Tracking landing page on about.gitlab.com][landing] +Time Tracking allows you to track estimates and time spent on issues and merge +requests within GitLab. ## Overview -- cgit v1.2.1 From 09a6141685ea0439c10377498046eaa0b036bae6 Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Tue, 3 Jan 2017 17:03:53 -0500 Subject: Fix haml-lint complain. app/views/shared/issuable/_sidebar.html.haml:79 [W] SpaceInsideHashAttributes: Hash attribute should end with one space before the closing brace --- app/views/shared/issuable/_sidebar.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c46710af758..ec9bcaf63dd 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -76,7 +76,7 @@ = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) - if issuable.has_attribute?(:time_estimate) #issuable-time-tracker.block - %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md')} + %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') } // Fallback while content is loading .title.hide-collapsed Time tracking -- cgit v1.2.1 From 63b36241945a7f9bb280f360b3b269de8c5be8f6 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Fri, 13 Jan 2017 15:35:24 -0500 Subject: Fix scss variable refs. --- app/assets/stylesheets/framework/variables.scss | 1 + app/assets/stylesheets/pages/issuable.scss | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index ee1c95fd373..3f95846522b 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -429,6 +429,7 @@ $help-shortcut-header-color: #333; */ $issues-today-bg: #f3fff2; $issues-today-border: #e1e8d5; +$compare-display-color: #888; /* * jQuery UI diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index c9014ac2906..1825c44e090 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -488,7 +488,7 @@ } &:hover svg { - fill: $gl-gray; + fill: $gl-text-color; } } @@ -535,10 +535,10 @@ .compare-display { font-size: 13px; - color: $gl-gray-light; + color: $compare-display-color; .compare-value { - color: $gl-gray; + color: $gl-text-color; } } } -- cgit v1.2.1 From 9844c1f222fa910fc5fe23de0821c83a095d84f9 Mon Sep 17 00:00:00 2001 From: Martin Cabrera Date: Mon, 16 Jan 2017 20:23:30 +0100 Subject: =?UTF-8?q?Refactored=20=E2=80=98from=E2=80=99=20and=20=E2=80=98to?= =?UTF-8?q?=E2=80=99=20variable=20preservation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/projects/compare_controller.rb | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 91793cc3650..2ab381f6850 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -27,8 +27,11 @@ class Projects::CompareController < Projects::ApplicationController def create if params[:from].blank? || params[:to].blank? flash[:alert] = "You must select from and to branches" - from_to_preservation = from_to_hash(params) - redirect_to namespace_project_compare_index_path(@project.namespace, @project, from_to_preservation) + from_to_vars = { + from: params[:from].presence, + to: params[:to].presence + } + redirect_to namespace_project_compare_index_path(@project.namespace, @project, from_to_vars) else redirect_to namespace_project_compare_path(@project.namespace, @project, params[:from], params[:to]) @@ -62,11 +65,4 @@ class Projects::CompareController < Projects::ApplicationController @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) end - - def from_to_hash(params) - return_hash = {} - return_hash[:from] = params[:from].presence - return_hash[:to] = params[:to].presence - return_hash - end -end +end \ No newline at end of file -- cgit v1.2.1 From 3f581e8d04fc9d1f128c57aebbc2d6791a9e4ccc Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 16 Jan 2017 14:55:30 -0500 Subject: Correctly keeps merge request tabs in place when editting value in collapsed sidebar Previously the CSS would presume the sidebar is open, which it is but as an overlay so the CSS was pushing the tabs when it shouldnt of been --- app/assets/stylesheets/framework/sidebar.scss | 6 +++++- changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 838f5442fff..f0b03710c79 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -236,9 +236,13 @@ header.header-sidebar-pinned { @media (min-width: $screen-md-min) { padding-right: $gutter_width; - .merge-request-tabs-holder.affix { + &:not(.with-overlay) .merge-request-tabs-holder.affix { right: $gutter_width; } + + &.with-overlay .merge-request-tabs-holder.affix { + right: $sidebar_collapsed_width; + } } &.with-overlay { diff --git a/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml b/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml new file mode 100644 index 00000000000..b8c7b78cf0d --- /dev/null +++ b/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml @@ -0,0 +1,4 @@ +--- +title: Fixed merge request tabs dont move when opening collapsed sidebar +merge_request: +author: -- cgit v1.2.1 From 856003cfea894b56784f0fea4ee64655e6c16f06 Mon Sep 17 00:00:00 2001 From: Martin Cabrera Date: Mon, 16 Jan 2017 23:17:24 +0100 Subject: refactor rubocop suggestion --- app/controllers/projects/compare_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 2ab381f6850..d32966645c8 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -65,4 +65,4 @@ class Projects::CompareController < Projects::ApplicationController @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) end -end \ No newline at end of file +end -- cgit v1.2.1 From 1b1bccdbee71ebd2fd7bf32f8f9ed826453037d9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 16 Jan 2017 12:36:49 -0500 Subject: Fixes big pipeline and small pipeline width problems Fixes tooltips text being outside the tooltip Adds MR id to changelog Fix linter error --- app/assets/stylesheets/pages/pipelines.scss | 10 ++++++---- .../unreleased/26667-pipeline-width-for-huge-pipeline.yml | 4 ++++ 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/26667-pipeline-width-for-huge-pipeline.yml diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 8861315d776..8dff22e32bd 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -44,8 +44,8 @@ .pipeline-info, .pipeline-commit, - .pipeline-actions, - .pipeline-stages { + .pipeline-stages, + .pipeline-actions { width: 20%; } } @@ -185,6 +185,7 @@ .stage-cell { font-size: 0; + padding: 10px 4px; > .stage-container > div > button > span > svg, > .stage-container > button > svg { @@ -202,8 +203,8 @@ position: relative; margin-right: 6px; - .tooltip { - white-space: nowrap; + .tooltip-inner { + padding: 3px 4px; } &:not(:last-child) { @@ -348,6 +349,7 @@ padding: $gl-padding; white-space: nowrap; transition: max-height 0.3s, padding 0.3s; + overflow: auto; .stage-column-list, .builds-container > ul { diff --git a/changelogs/unreleased/26667-pipeline-width-for-huge-pipeline.yml b/changelogs/unreleased/26667-pipeline-width-for-huge-pipeline.yml new file mode 100644 index 00000000000..08dcc5c3e8c --- /dev/null +++ b/changelogs/unreleased/26667-pipeline-width-for-huge-pipeline.yml @@ -0,0 +1,4 @@ +--- +title: Fixes big pipeline and small pipeline width problems and tooltips text being outside the tooltip +merge_request: 8593 +author: -- cgit v1.2.1 From 959f65a0f029a4b9bcd510d6596a492f389df315 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 16 Jan 2017 20:35:02 -0500 Subject: Correct documentation for `data_attribute` method Also break up a long line, just 'cause. --- lib/banzai/filter/reference_filter.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 84bfeac8041..ab7af1cad21 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -20,10 +20,10 @@ module Banzai # Examples: # # data_attribute(project: 1, issue: 2) - # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" + # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" # # data_attribute(project: 3, merge_request: 4) - # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" + # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" # # Returns a String def data_attribute(attributes = {}) @@ -31,7 +31,9 @@ module Banzai attributes[:reference_type] ||= self.class.reference_type attributes.delete(:original) if context[:no_original_data] - attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ") + attributes.map do |key, value| + %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") + end.join(' ') end def escape_once(html) -- cgit v1.2.1 From 632d22acf765339a35e4552b43e680398bddbf65 Mon Sep 17 00:00:00 2001 From: "P.S.V.R" Date: Mon, 18 Apr 2016 16:27:51 +0800 Subject: Mark Commit persisted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/models/commit.rb | 8 ++++++++ app/views/projects/commit/_change.html.haml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 0b924b063a4..3365f4ffdbf 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -318,6 +318,14 @@ class Commit Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) end + def persisted? + true + end + + def touch + # no-op but needs to be defined since #persisted? is defined + end + private def commit_reference(from_project, referable_commit_id, full: false) diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 12e4280d344..990908211de 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -13,7 +13,7 @@ %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title== #{label} this #{commit.change_type_title(current_user)} .modal-body - = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do + = form_tag [type.underscore, @project.namespace, @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 -- cgit v1.2.1 From 2ac0178564e5593b6f666df79ac1b109dcabaf32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 17 Jan 2017 01:22:01 -0500 Subject: Remove some useless require_relative statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/gitlab/git/blame.rb | 2 -- lib/gitlab/git/blob.rb | 3 --- lib/gitlab/git/repository.rb | 2 -- 3 files changed, 7 deletions(-) diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb index 2913230e979..58193391926 100644 --- a/lib/gitlab/git/blame.rb +++ b/lib/gitlab/git/blame.rb @@ -1,5 +1,3 @@ -require_relative 'encoding_helper' - module Gitlab module Git class Blame diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 4a623311c14..b742d9e1e4b 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -1,6 +1,3 @@ -require_relative 'encoding_helper' -require_relative 'path_helper' - module Gitlab module Git class Blob diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 79b23d59b3a..7068e68a855 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1,6 +1,4 @@ # Gitlab::Git::Repository is a wrapper around native Rugged::Repository object -require_relative 'encoding_helper' -require_relative 'path_helper' require 'forwardable' require 'tempfile' require 'forwardable' -- cgit v1.2.1 From 3268e3779166c7ddf47ecc0d78397cd75cf2f0e8 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 18 Nov 2016 16:38:02 +0100 Subject: WIP - started refactoring cycle analytics median stuff into stages --- app/models/cycle_analytics.rb | 47 ++++---------------------- lib/gitlab/cycle_analytics/base_event.rb | 8 ++--- lib/gitlab/cycle_analytics/base_stage.rb | 24 +++++++++++++ lib/gitlab/cycle_analytics/code_event.rb | 1 - lib/gitlab/cycle_analytics/code_stage.rb | 11 ++++++ lib/gitlab/cycle_analytics/events_query.rb | 9 ++--- lib/gitlab/cycle_analytics/issue_event.rb | 1 - lib/gitlab/cycle_analytics/issue_stage.rb | 12 +++++++ lib/gitlab/cycle_analytics/metrics_fetcher.rb | 3 +- lib/gitlab/cycle_analytics/plan_event.rb | 1 - lib/gitlab/cycle_analytics/plan_stage.rb | 12 +++++++ lib/gitlab/cycle_analytics/production_event.rb | 1 - lib/gitlab/cycle_analytics/production_stage.rb | 11 ++++++ lib/gitlab/cycle_analytics/review_event.rb | 1 - lib/gitlab/cycle_analytics/review_stage.rb | 11 ++++++ lib/gitlab/cycle_analytics/staging_event.rb | 1 - lib/gitlab/cycle_analytics/staging_stage.rb | 11 ++++++ lib/gitlab/cycle_analytics/test_event.rb | 1 - lib/gitlab/cycle_analytics/test_stage.rb | 11 ++++++ 19 files changed, 118 insertions(+), 59 deletions(-) create mode 100644 lib/gitlab/cycle_analytics/base_stage.rb create mode 100644 lib/gitlab/cycle_analytics/code_stage.rb create mode 100644 lib/gitlab/cycle_analytics/issue_stage.rb create mode 100644 lib/gitlab/cycle_analytics/plan_stage.rb create mode 100644 lib/gitlab/cycle_analytics/production_stage.rb create mode 100644 lib/gitlab/cycle_analytics/review_stage.rb create mode 100644 lib/gitlab/cycle_analytics/staging_stage.rb create mode 100644 lib/gitlab/cycle_analytics/test_stage.rb diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index ba4ee6fcf9d..5e33273c9ba 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -1,17 +1,17 @@ class CycleAnalytics STAGES = %i[issue plan code test review staging production].freeze - def initialize(project, current_user, from:) + def initialize(project, from:) @project = project - @current_user = current_user - @from = from - @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil) + @options = options end def summary - @summary ||= Summary.new(@project, @current_user, from: @from) + @summary ||= Summary.new(@project, from: @options[:from]) end + def method_missing(method_sym, *arguments, &block) + classify_stage(method_sym).new(project: @project, options: @options, stage: method_sym) def permissions(user:) Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) end @@ -23,40 +23,7 @@ class CycleAnalytics Issue::Metrics.arel_table[:first_added_to_board_at]]) end - def plan - @fetcher.calculate_metric(:plan, - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]], - Issue::Metrics.arel_table[:first_mentioned_in_commit_at]) - end - - def code - @fetcher.calculate_metric(:code, - Issue::Metrics.arel_table[:first_mentioned_in_commit_at], - MergeRequest.arel_table[:created_at]) - end - - def test - @fetcher.calculate_metric(:test, - MergeRequest::Metrics.arel_table[:latest_build_started_at], - MergeRequest::Metrics.arel_table[:latest_build_finished_at]) - end - - def review - @fetcher.calculate_metric(:review, - MergeRequest.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:merged_at]) - end - - def staging - @fetcher.calculate_metric(:staging, - MergeRequest::Metrics.arel_table[:merged_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) - end - - def production - @fetcher.calculate_metric(:production, - Issue.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + def classify_stage(method_sym) + "Gitlab::CycleAnalytics::#{method_sym.to_s.capitalize}Stage".constantize end end diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb index 53a148ad703..c87841c119a 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event.rb @@ -5,10 +5,10 @@ module Gitlab attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query - def initialize(project:, options:) - @query = EventsQuery.new(project: project, options: options) - @project = project - @options = options + def initialize(fetcher:, stage:) + @query = EventsQuery.new(fetcher: fetcher) + @project = fetcher.project + @stage = stage end def fetch diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb new file mode 100644 index 00000000000..70f1e1018c9 --- /dev/null +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -0,0 +1,24 @@ +module Gitlab + module CycleAnalytics + class BaseStage + def initialize(project:, options:, stage: stage) + @project = project + @options = options + @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, + from: options[:from], + branch: options[:branch]) + @stage = stage + end + + def events + event_class.new(fetcher: @fetcher, stage: @stage).fetch + end + + private + + def event_class + "Gitlab::CycleAnalytics::#{@stage.to_s.capitalize}Event".constantize + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/code_event.rb index 2afdf0b8518..68251630e08 100644 --- a/lib/gitlab/cycle_analytics/code_event.rb +++ b/lib/gitlab/cycle_analytics/code_event.rb @@ -4,7 +4,6 @@ module Gitlab include MergeRequestAllowed def initialize(*args) - @stage = :code @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] @end_time_attrs = mr_table[:created_at] @projections = [mr_table[:title], diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb new file mode 100644 index 00000000000..9d28393ce53 --- /dev/null +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -0,0 +1,11 @@ +module Gitlab + module CycleAnalytics + class CodeStage < BaseStage + def median + @fetcher.calculate_metric(:code, + Issue::Metrics.arel_table[:first_mentioned_in_commit_at], + MergeRequest.arel_table[:created_at]) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/events_query.rb b/lib/gitlab/cycle_analytics/events_query.rb index 2418832ccc2..e2b79384c9b 100644 --- a/lib/gitlab/cycle_analytics/events_query.rb +++ b/lib/gitlab/cycle_analytics/events_query.rb @@ -1,13 +1,8 @@ module Gitlab module CycleAnalytics class EventsQuery - attr_reader :project - - def initialize(project:, options: {}) - @project = project - @from = options[:from] - @branch = options[:branch] - @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: @from, branch: @branch) + def initialize(fetcher:) + @fetcher = fetcher end def execute(stage_class) diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb index 705b7e5ce24..76e8decf36e 100644 --- a/lib/gitlab/cycle_analytics/issue_event.rb +++ b/lib/gitlab/cycle_analytics/issue_event.rb @@ -4,7 +4,6 @@ module Gitlab include IssueAllowed def initialize(*args) - @stage = :issue @start_time_attrs = issue_table[:created_at] @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at], issue_metrics_table[:first_added_to_board_at]] diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb new file mode 100644 index 00000000000..6793cc77976 --- /dev/null +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -0,0 +1,12 @@ +module Gitlab + module CycleAnalytics + class IssueStage < BaseStage + def median + @fetcher.calculate_metric(:issue, + Issue.arel_table[:created_at], + [Issue::Metrics.arel_table[:first_associated_with_milestone_at], + Issue::Metrics.arel_table[:first_added_to_board_at]]) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb index b71e8735e27..51835bbde24 100644 --- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb +++ b/lib/gitlab/cycle_analytics/metrics_fetcher.rb @@ -5,10 +5,11 @@ module Gitlab include Gitlab::Database::DateTime include MetricsTables + attr_reader :project + DEPLOYMENT_METRIC_STAGES = %i[production staging] def initialize(project:, from:, branch:) - @project = project @project = project @from = from @branch = branch diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event.rb index 7c3f0e9989f..4b06143495b 100644 --- a/lib/gitlab/cycle_analytics/plan_event.rb +++ b/lib/gitlab/cycle_analytics/plan_event.rb @@ -2,7 +2,6 @@ module Gitlab module CycleAnalytics class PlanEvent < BaseEvent def initialize(*args) - @stage = :plan @start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at] @end_time_attrs = [issue_metrics_table[:first_added_to_board_at], issue_metrics_table[:first_mentioned_in_commit_at]] diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb new file mode 100644 index 00000000000..772237087c0 --- /dev/null +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -0,0 +1,12 @@ +module Gitlab + module CycleAnalytics + class PlanStage < BaseStage + def median + @fetcher.calculate_metric(:plan, + [Issue::Metrics.arel_table[:first_associated_with_milestone_at], + Issue::Metrics.arel_table[:first_added_to_board_at]], + Issue::Metrics.arel_table[:first_mentioned_in_commit_at]) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/production_event.rb index 4868c3c6237..c03cd4f4909 100644 --- a/lib/gitlab/cycle_analytics/production_event.rb +++ b/lib/gitlab/cycle_analytics/production_event.rb @@ -4,7 +4,6 @@ module Gitlab include IssueAllowed def initialize(*args) - @stage = :production @start_time_attrs = issue_table[:created_at] @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] @projections = [issue_table[:title], diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb new file mode 100644 index 00000000000..2fb087a8cac --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -0,0 +1,11 @@ +module Gitlab + module CycleAnalytics + class ProductionStage < BaseStage + def median + @fetcher.calculate_metric(:production, + Issue.arel_table[:created_at], + MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/review_event.rb index b394a02cc52..3f9ffa9657b 100644 --- a/lib/gitlab/cycle_analytics/review_event.rb +++ b/lib/gitlab/cycle_analytics/review_event.rb @@ -4,7 +4,6 @@ module Gitlab include MergeRequestAllowed def initialize(*args) - @stage = :review @start_time_attrs = mr_table[:created_at] @end_time_attrs = mr_metrics_table[:merged_at] @projections = [mr_table[:title], diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb new file mode 100644 index 00000000000..ec9f07319e8 --- /dev/null +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -0,0 +1,11 @@ +module Gitlab + module CycleAnalytics + class ReviewStage < BaseStage + def median + @fetcher.calculate_metric(:review, + MergeRequest.arel_table[:created_at], + MergeRequest::Metrics.arel_table[:merged_at]) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event.rb index a1f30b716f6..eae18b447f0 100644 --- a/lib/gitlab/cycle_analytics/staging_event.rb +++ b/lib/gitlab/cycle_analytics/staging_event.rb @@ -2,7 +2,6 @@ module Gitlab module CycleAnalytics class StagingEvent < BaseEvent def initialize(*args) - @stage = :staging @start_time_attrs = mr_metrics_table[:merged_at] @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] @projections = [build_table[:id]] diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb new file mode 100644 index 00000000000..9c67a2aa6fe --- /dev/null +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -0,0 +1,11 @@ +module Gitlab + module CycleAnalytics + class StagingStage < BaseStage + def median + @fetcher.calculate_metric(:staging, + MergeRequest::Metrics.arel_table[:merged_at], + MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb index d553d0b5aec..d0736672adf 100644 --- a/lib/gitlab/cycle_analytics/test_event.rb +++ b/lib/gitlab/cycle_analytics/test_event.rb @@ -4,7 +4,6 @@ module Gitlab def initialize(*args) super(*args) - @stage = :test @start_time_attrs = mr_metrics_table[:latest_build_started_at] @end_time_attrs = mr_metrics_table[:latest_build_finished_at] end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb new file mode 100644 index 00000000000..6bedfdbba61 --- /dev/null +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -0,0 +1,11 @@ +module Gitlab + module CycleAnalytics + class TestStage < BaseStage + def median + @fetcher.calculate_metric(:test, + MergeRequest::Metrics.arel_table[:latest_build_started_at], + MergeRequest::Metrics.arel_table[:latest_build_finished_at]) + end + end + end +end -- cgit v1.2.1 From a998276223510ceee67f686dfc3ef536c0252c5a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 21 Nov 2016 17:15:25 +0100 Subject: added analytics stage serializer and moved some info to the stage classes from the controller --- app/serializers/analytics_stage_entity.rb | 12 ++++++++++++ app/serializers/analytics_stage_serializer.rb | 3 +++ lib/gitlab/cycle_analytics/base_stage.rb | 2 ++ lib/gitlab/cycle_analytics/code_stage.rb | 6 ++++++ lib/gitlab/cycle_analytics/issue_stage.rb | 6 ++++++ lib/gitlab/cycle_analytics/plan_stage.rb | 6 ++++++ lib/gitlab/cycle_analytics/production_stage.rb | 6 ++++++ lib/gitlab/cycle_analytics/review_stage.rb | 6 ++++++ lib/gitlab/cycle_analytics/staging_stage.rb | 6 ++++++ lib/gitlab/cycle_analytics/test_stage.rb | 6 ++++++ 10 files changed, 59 insertions(+) create mode 100644 app/serializers/analytics_stage_entity.rb create mode 100644 app/serializers/analytics_stage_serializer.rb diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb new file mode 100644 index 00000000000..72a587c8c1d --- /dev/null +++ b/app/serializers/analytics_stage_entity.rb @@ -0,0 +1,12 @@ +class AnalyticsStageEntity < Grape::Entity + include EntityDateHelper + + expose :stage, as: :title do |object| + object[:stage].to_s.capitalize + end + expose :description + + expose :median, as: :value do |stage| + stage[:median] && !stage[:median].zero? ? distance_of_time_in_words(stage[:median]) : nil + end +end diff --git a/app/serializers/analytics_stage_serializer.rb b/app/serializers/analytics_stage_serializer.rb new file mode 100644 index 00000000000..613cf6874d8 --- /dev/null +++ b/app/serializers/analytics_stage_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsStageSerializer < BaseSerializer + entity AnalyticsStageEntity +end diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index 70f1e1018c9..49d1e6304a9 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -1,6 +1,8 @@ module Gitlab module CycleAnalytics class BaseStage + attr_reader :stage, :description + def initialize(project:, options:, stage: stage) @project = project @options = options diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index 9d28393ce53..f72989c9a72 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -1,6 +1,12 @@ module Gitlab module CycleAnalytics class CodeStage < BaseStage + def initialize(*args) + super(*args) + + @description = "Time until first merge request" + end + def median @fetcher.calculate_metric(:code, Issue::Metrics.arel_table[:first_mentioned_in_commit_at], diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index 6793cc77976..a2ada238cd2 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -1,6 +1,12 @@ module Gitlab module CycleAnalytics class IssueStage < BaseStage + def initialize(*args) + super(*args) + + @description = "Time before an issue gets scheduled" + end + def median @fetcher.calculate_metric(:issue, Issue.arel_table[:created_at], diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index 772237087c0..c836068c4ef 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -1,6 +1,12 @@ module Gitlab module CycleAnalytics class PlanStage < BaseStage + def initialize(*args) + super(*args) + + @description = "Time before an issue starts implementation" + end + def median @fetcher.calculate_metric(:plan, [Issue::Metrics.arel_table[:first_associated_with_milestone_at], diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb index 2fb087a8cac..d46d37e1acc 100644 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -1,6 +1,12 @@ module Gitlab module CycleAnalytics class ProductionStage < BaseStage + def initialize(*args) + super(*args) + + @description = "From issue creation until deploy to production" + end + def median @fetcher.calculate_metric(:production, Issue.arel_table[:created_at], diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index ec9f07319e8..4159ba5d70d 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -1,6 +1,12 @@ module Gitlab module CycleAnalytics class ReviewStage < BaseStage + def initialize(*args) + super(*args) + + @description = "Time between merge request creation and merge/close" + end + def median @fetcher.calculate_metric(:review, MergeRequest.arel_table[:created_at], diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index 9c67a2aa6fe..cb4398f15ac 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -1,6 +1,12 @@ module Gitlab module CycleAnalytics class StagingStage < BaseStage + def initialize(*args) + super(*args) + + @description = "From merge request merge until deploy to production" + end + def median @fetcher.calculate_metric(:staging, MergeRequest::Metrics.arel_table[:merged_at], diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index 6bedfdbba61..3ab93bebd87 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -1,6 +1,12 @@ module Gitlab module CycleAnalytics class TestStage < BaseStage + def initialize(*args) + super(*args) + + @description = "Total test time for all commits/merges" + end + def median @fetcher.calculate_metric(:test, MergeRequest::Metrics.arel_table[:latest_build_started_at], -- cgit v1.2.1 From dc6ea14b0d11a5e73e81c95ef723f0c1af69215b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 22 Nov 2016 10:33:19 +0100 Subject: fixed stage entity and added missing stage specs --- app/serializers/analytics_stage_entity.rb | 4 +-- lib/gitlab/cycle_analytics/base_stage.rb | 6 ++++- spec/lib/gitlab/cycle_analytics/code_stage_spec.rb | 8 ++++++ .../lib/gitlab/cycle_analytics/issue_stage_spec.rb | 8 ++++++ spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb | 8 ++++++ .../cycle_analytics/production_stage_spec.rb | 8 ++++++ .../gitlab/cycle_analytics/review_stage_spec.rb | 8 ++++++ .../gitlab/cycle_analytics/shared_stage_spec.rb | 30 ++++++++++++++++++++++ .../gitlab/cycle_analytics/staging_stage_spec.rb | 8 ++++++ spec/lib/gitlab/cycle_analytics/test_stage_spec.rb | 8 ++++++ 10 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 spec/lib/gitlab/cycle_analytics/code_stage_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/production_stage_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/review_stage_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/test_stage_spec.rb diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb index 72a587c8c1d..d454a4937f4 100644 --- a/app/serializers/analytics_stage_entity.rb +++ b/app/serializers/analytics_stage_entity.rb @@ -2,11 +2,11 @@ class AnalyticsStageEntity < Grape::Entity include EntityDateHelper expose :stage, as: :title do |object| - object[:stage].to_s.capitalize + object.stage.to_s.capitalize end expose :description expose :median, as: :value do |stage| - stage[:median] && !stage[:median].zero? ? distance_of_time_in_words(stage[:median]) : nil + stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil end end diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index 49d1e6304a9..27971bfc093 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -3,7 +3,7 @@ module Gitlab class BaseStage attr_reader :stage, :description - def initialize(project:, options:, stage: stage) + def initialize(project:, options:, stage:) @project = project @options = options @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, @@ -16,6 +16,10 @@ module Gitlab event_class.new(fetcher: @fetcher, stage: @stage).fetch end + def median_data + AnalyticsStageSerializer.new.represent(self).as_json + end + private def event_class diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb new file mode 100644 index 00000000000..e8fc67acf05 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::CodeStage do + let(:stage_name) { :code } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb new file mode 100644 index 00000000000..3127f01989d --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::IssueStage do + let(:stage_name) { :issue } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb new file mode 100644 index 00000000000..4c715921ad6 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::PlanStage do + let(:stage_name) { :plan } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb new file mode 100644 index 00000000000..916684b81eb --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::ProductionStage do + let(:stage_name) { :production } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb new file mode 100644 index 00000000000..1412c8dfa08 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::ReviewStage do + let(:stage_name) { :review } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb new file mode 100644 index 00000000000..dd1ef4fc129 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +shared_examples 'base stage' do + let(:stage) { described_class.new(project: double, options: {}, stage: stage_name) } + + before do + allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:calculate_metric).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEvent).to receive(:event_result).and_return({}) + end + + it 'has the median data value' do + expect(stage.median_data[:value]).not_to be_nil + end + + it 'has the median data stage' do + expect(stage.median_data[:title]).not_to be_nil + end + + it 'has the median data description' do + expect(stage.median_data[:description]).not_to be_nil + end + + it 'has the stage' do + expect(stage.stage).to eq(stage_name) + end + + it 'has the events' do + expect(stage.events).not_to be_nil + end +end diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb new file mode 100644 index 00000000000..8154b3ac701 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::StagingStage do + let(:stage_name) { :staging } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb new file mode 100644 index 00000000000..eacde22cd56 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::TestStage do + let(:stage_name) { :test } + + it_behaves_like 'base stage' +end -- cgit v1.2.1 From fc6f8f20562ad761c034ffff076d329a3e9e8f4d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 22 Nov 2016 11:46:02 +0100 Subject: added new summary serializers and refactor all of the summary stuff into separate logical classes --- .../projects/cycle_analytics_controller.rb | 50 +++------------------- app/models/cycle_analytics.rb | 2 +- app/models/cycle_analytics/summary.rb | 43 ------------------- app/serializers/analytics_summary_entity.rb | 7 +++ app/serializers/analytics_summary_serializer.rb | 3 ++ lib/gitlab/cycle_analytics/summary.rb | 36 ++++++++++++++++ lib/gitlab/cycle_analytics/summary/base.rb | 20 +++++++++ lib/gitlab/cycle_analytics/summary/commit.rb | 36 ++++++++++++++++ lib/gitlab/cycle_analytics/summary/deploy.rb | 11 +++++ lib/gitlab/cycle_analytics/summary/issue.rb | 15 +++++++ .../serializers/analytics_stage_serializer_spec.rb | 24 +++++++++++ .../analytics_summary_serializer_spec.rb | 24 +++++++++++ 12 files changed, 182 insertions(+), 89 deletions(-) create mode 100644 app/serializers/analytics_summary_entity.rb create mode 100644 app/serializers/analytics_summary_serializer.rb create mode 100644 lib/gitlab/cycle_analytics/summary.rb create mode 100644 lib/gitlab/cycle_analytics/summary/base.rb create mode 100644 lib/gitlab/cycle_analytics/summary/commit.rb create mode 100644 lib/gitlab/cycle_analytics/summary/deploy.rb create mode 100644 lib/gitlab/cycle_analytics/summary/issue.rb create mode 100644 spec/serializers/analytics_stage_serializer_spec.rb create mode 100644 spec/serializers/analytics_summary_serializer_spec.rb diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index ac639ef015b..73fe3c9c4c9 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -8,10 +8,6 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController def show @cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params)) - stats_values, cycle_analytics_json = generate_cycle_analytics_data - - @cycle_analytics_no_data = stats_values.blank? - respond_to do |format| format.html format.json { render json: cycle_analytics_json } @@ -26,47 +22,11 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController { start_date: params[:cycle_analytics][:start_date] } end - def generate_cycle_analytics_data - stats_values = [] - - cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"], - [:plan, "Plan", "Related Commits", "Time before an issue starts implementation"], - [:code, "Code", "Related Merge Requests", "Time spent coding"], - [:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"], - [:review, "Review", "Relative Merged Requests", "The time taken to review the code"], - [:staging, "Staging", "Relative Deployed Builds", "The time taken in staging"], - [:production, "Production", "Related Issues", "The total time taken from idea to production"]] - - stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_legend, stage_description)| - value = @cycle_analytics.send(stage_method).presence - - stats_values << value.abs if value - - stats << { - title: stage_text, - description: stage_description, - legend: stage_legend, - value: value && !value.zero? ? distance_of_time_in_words(value) : nil - } - - stats - end - - issues = @cycle_analytics.summary.new_issues - commits = @cycle_analytics.summary.commits - deploys = @cycle_analytics.summary.deploys - - summary = [ - { title: "New Issue".pluralize(issues), value: issues }, - { title: "Commit".pluralize(commits), value: commits }, - { title: "Deploy".pluralize(deploys), value: deploys } - ] - - cycle_analytics_hash = { summary: summary, - stats: stats, - permissions: @cycle_analytics.permissions(user: current_user) + def cycle_analytics_json + { + summary: @cycle_analytics.summary, + stats: nil, # TODO + permissions: @cycle_analytics.permissions(user: current_user)# TODO } - - [stats_values, cycle_analytics_hash] end end diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index 5e33273c9ba..e0f9690f1f4 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -7,7 +7,7 @@ class CycleAnalytics end def summary - @summary ||= Summary.new(@project, from: @options[:from]) + @summary ||= Gitlab::CycleAnalytics::Summary.new(@project, from: @options[:from]).data end def method_missing(method_sym, *arguments, &block) diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb index c9910d8cd09..e69de29bb2d 100644 --- a/app/models/cycle_analytics/summary.rb +++ b/app/models/cycle_analytics/summary.rb @@ -1,43 +0,0 @@ -class CycleAnalytics - class Summary - def initialize(project, current_user, from:) - @project = project - @current_user = current_user - @from = from - end - - def new_issues - IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count - end - - def commits - ref = @project.default_branch.presence - count_commits_for(ref) - end - - def deploys - @project.deployments.where("created_at > ?", @from).count - end - - private - - # Don't use the `Gitlab::Git::Repository#log` method, because it enforces - # a limit. Since we need a commit count, we _can't_ enforce a limit, so - # the easiest way forward is to replicate the relevant portions of the - # `log` function here. - def count_commits_for(ref) - return unless ref - - repository = @project.repository.raw_repository - sha = @project.repository.commit(ref).sha - - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{repository.path} log) - cmd << '--format=%H' - cmd << "--after=#{@from.iso8601}" - cmd << sha - - raw_output = IO.popen(cmd) { |io| io.read } - raw_output.lines.count - end - end -end diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb new file mode 100644 index 00000000000..91803ec07f5 --- /dev/null +++ b/app/serializers/analytics_summary_entity.rb @@ -0,0 +1,7 @@ +class AnalyticsSummaryEntity < Grape::Entity + expose :value, safe: true + + expose :title do |object| + object.title.pluralize(object.value) + end +end diff --git a/app/serializers/analytics_summary_serializer.rb b/app/serializers/analytics_summary_serializer.rb new file mode 100644 index 00000000000..c87a24aa47c --- /dev/null +++ b/app/serializers/analytics_summary_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsSummarySerializer < BaseSerializer + entity AnalyticsSummaryEntity +end diff --git a/lib/gitlab/cycle_analytics/summary.rb b/lib/gitlab/cycle_analytics/summary.rb new file mode 100644 index 00000000000..7d172855a94 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary.rb @@ -0,0 +1,36 @@ +module Gitlab + module CycleAnalytics + module Summary + extend self + + def initialize(project, from:) + @project = project + @from = from + end + + def data + [serialize(issue), + serialize(commit), + serialize(deploy)] + end + + private + + def serialize(summary_object) + AnalyticsSummarySerializer.new.represent(summary_object).as_json + end + + def issue + Summary::Issue.new(project: @project, from: @from) + end + + def deploy + Summary::Deploy.new(project: @project, from: @from) + end + + def commit + Summary::Commit.new(project: @project, from: @from) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/base.rb b/lib/gitlab/cycle_analytics/summary/base.rb new file mode 100644 index 00000000000..1bc4ff00b99 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/base.rb @@ -0,0 +1,20 @@ +module Gitlab + module CycleAnalytics + module Summary + class Base + def initialize(project:, from:) + @project = project + @from = from + end + + def title + self.name + end + + def value + raise NotImplementedError.new("Expected #{self.name} to implement value") + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb new file mode 100644 index 00000000000..ec3c067c0be --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/commit.rb @@ -0,0 +1,36 @@ +module Gitlab + module CycleAnalytics + module Summary + class Commit < Base + def value + @value ||= count_commits + end + + private + + # Don't use the `Gitlab::Git::Repository#log` method, because it enforces + # a limit. Since we need a commit count, we _can't_ enforce a limit, so + # the easiest way forward is to replicate the relevant portions of the + # `log` function here. + def count_commits + return unless ref + + repository = @project.repository.raw_repository + sha = @project.repository.commit(ref).sha + + cmd = %W(git --git-dir=#{repository.path} log) + cmd << '--format=%H' + cmd << "--after=#{@from.iso8601}" + cmd << sha + + raw_output = IO.popen(cmd) { |io| io.read } + raw_output.lines.count + end + + def ref + @ref ||= @project.default_branch.presence + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb new file mode 100644 index 00000000000..06032e9200e --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/deploy.rb @@ -0,0 +1,11 @@ +module Gitlab + module CycleAnalytics + module Summary + class Deploy < Base + def value + @value ||= @project.deployments.where("created_at > ?", @from).count + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb new file mode 100644 index 00000000000..7d62164aae3 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/issue.rb @@ -0,0 +1,15 @@ +module Gitlab + module CycleAnalytics + module Summary + class Issue < Base + def title + 'New Issue' + end + + def value + @value ||= @project.issues.created_after(@from).count + end + end + end + end +end diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb new file mode 100644 index 00000000000..0f2d534e714 --- /dev/null +++ b/spec/serializers/analytics_stage_serializer_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe AnalyticsStageSerializer do + let(:serializer) do + described_class + .new.represent(resource) + end + + let(:json) { serializer.as_json } + let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}, stage: :code) } + + before do + allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:calculate_metric).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEvent).to receive(:event_result).and_return({}) + end + + it 'it generates payload for single object' do + expect(json).to be_an_instance_of Hash + end + + it 'contains important elements of AnalyticsStage' do + expect(json).to include(:title, :description, :value) + end +end diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb new file mode 100644 index 00000000000..e08e3f88710 --- /dev/null +++ b/spec/serializers/analytics_summary_serializer_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe AnalyticsSummarySerializer do + let(:serializer) do + described_class + .new.represent(resource) + end + + let(:json) { serializer.as_json } + let(:project) { create(:empty_project) } + let(:resource) { Gitlab::CycleAnalytics::Summary::Issue.new(project: double, from: 1.day.ago) } + + before do + allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue).to receive(:value).and_return(1.12) + end + + it 'it generates payload for single object' do + expect(json).to be_an_instance_of Hash + end + + it 'contains important elements of AnalyticsStage' do + expect(json).to include(:title, :value) + end +end -- cgit v1.2.1 From 8639ea1b0315045c0e4a5ad8d6419903507850c3 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 22 Nov 2016 12:01:21 +0100 Subject: fix bad merge --- app/models/cycle_analytics.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index e0f9690f1f4..9681d34f2d1 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -12,15 +12,17 @@ class CycleAnalytics def method_missing(method_sym, *arguments, &block) classify_stage(method_sym).new(project: @project, options: @options, stage: method_sym) + end + def permissions(user:) Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) end def issue @fetcher.calculate_metric(:issue, - Issue.arel_table[:created_at], - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]]) + Issue.arel_table[:created_at], + [Issue::Metrics.arel_table[:first_associated_with_milestone_at], + Issue::Metrics.arel_table[:first_added_to_board_at]]) end def classify_stage(method_sym) -- cgit v1.2.1 From 02e1e4819234662faddd7d8eb5c54d9bfdf9e7e6 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 22 Nov 2016 14:29:25 +0100 Subject: more refactoring and fixing old specs --- app/controllers/concerns/cycle_analytics_params.rb | 4 ++++ .../projects/cycle_analytics/events_controller.rb | 4 ---- .../projects/cycle_analytics_controller.rb | 8 ++++--- app/models/cycle_analytics.rb | 26 +++++++++++++--------- lib/gitlab/cycle_analytics/summary.rb | 18 +++------------ spec/models/cycle_analytics/code_spec.rb | 18 +++++++-------- spec/models/cycle_analytics/issue_spec.rb | 2 +- spec/models/cycle_analytics/plan_spec.rb | 2 +- spec/models/cycle_analytics/production_spec.rb | 2 +- spec/models/cycle_analytics/review_spec.rb | 2 +- spec/models/cycle_analytics/staging_spec.rb | 2 +- spec/models/cycle_analytics/summary_spec.rb | 2 +- spec/models/cycle_analytics/test_spec.rb | 2 +- .../cycle_analytics_helpers/test_generation.rb | 7 +++++- 14 files changed, 50 insertions(+), 49 deletions(-) diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb index 2aaf8f2b451..91456561a17 100644 --- a/app/controllers/concerns/cycle_analytics_params.rb +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -1,6 +1,10 @@ module CycleAnalyticsParams extend ActiveSupport::Concern + def options + @options ||= { from: start_date(events_params), current_user: current_user } + end + def start_date(params) params[:start_date] == '30' ? 30.days.ago : 90.days.ago end diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index 13b3eec761f..5e9524b15db 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -51,10 +51,6 @@ module Projects @events ||= Gitlab::CycleAnalytics::Events.new(project: project, options: options) end - def options - @options ||= { from: start_date(events_params), current_user: current_user } - end - def events_params return {} unless params[:events].present? diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index 73fe3c9c4c9..93dbe2819e7 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -6,7 +6,9 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController before_action :authorize_read_cycle_analytics! def show - @cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params)) + @cycle_analytics = ::CycleAnalytics.new(@project, options: options) + + @cycle_analytics_no_data = @cycle_analytics.no_stats? respond_to do |format| format.html @@ -25,8 +27,8 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController def cycle_analytics_json { summary: @cycle_analytics.summary, - stats: nil, # TODO - permissions: @cycle_analytics.permissions(user: current_user)# TODO + stats: @cycle_analytics.stats, + permissions: @cycle_analytics.permissions(user: current_user) } end end diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index 9681d34f2d1..00e9f7c7d5c 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -1,7 +1,7 @@ class CycleAnalytics STAGES = %i[issue plan code test review staging production].freeze - def initialize(project, from:) + def initialize(project, options:) @project = project @options = options end @@ -10,22 +10,28 @@ class CycleAnalytics @summary ||= Gitlab::CycleAnalytics::Summary.new(@project, from: @options[:from]).data end - def method_missing(method_sym, *arguments, &block) - classify_stage(method_sym).new(project: @project, options: @options, stage: method_sym) + def stats + @stats ||= stats_per_stage + end + + def no_stats? + stats.map(&:value).compact.empty? end def permissions(user:) Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) end - def issue - @fetcher.calculate_metric(:issue, - Issue.arel_table[:created_at], - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]]) + private + + def stats_per_stage + STAGES.map do |stage_name| + classify_stage(method_sym).new(project: @project, options: @options, stage: stage_name).median_data + end end - def classify_stage(method_sym) - "Gitlab::CycleAnalytics::#{method_sym.to_s.capitalize}Stage".constantize + def classify_stage(stage_name) + "Gitlab::CycleAnalytics::#{stage_name.to_s.capitalize}Stage".constantize end + end diff --git a/lib/gitlab/cycle_analytics/summary.rb b/lib/gitlab/cycle_analytics/summary.rb index 7d172855a94..5f0103c9d5a 100644 --- a/lib/gitlab/cycle_analytics/summary.rb +++ b/lib/gitlab/cycle_analytics/summary.rb @@ -9,9 +9,9 @@ module Gitlab end def data - [serialize(issue), - serialize(commit), - serialize(deploy)] + [serialize(Summary::Issue.new(project: @project, from: @from)), + serialize(Summary::Commit.new(project: @project, from: @from)), + serialize(Summary::Deploy.new(project: @project, from: @from))] end private @@ -19,18 +19,6 @@ module Gitlab def serialize(summary_object) AnalyticsSummarySerializer.new.represent(summary_object).as_json end - - def issue - Summary::Issue.new(project: @project, from: @from) - end - - def deploy - Summary::Deploy.new(project: @project, from: @from) - end - - def commit - Summary::Commit.new(project: @project, from: @from) - end end end end diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 7771785ead3..4838b57e353 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } context 'with deployment' do generate_cycle_analytics_spec( @@ -16,10 +16,10 @@ describe 'CycleAnalytics#code', feature: true do -> (context, data) do context.create_commit_referencing_issue(data[:issue]) end]], - end_time_conditions: [["merge request that closes issue is created", - -> (context, data) do - context.create_merge_request_closing_issue(data[:issue]) - end]], + end_time_conditions: [["merge request that closes issue is created", + -> (context, data) do + context.create_merge_request_closing_issue(data[:issue]) + end]], post_fn: -> (context, data) do context.merge_merge_requests_closing_issue(data[:issue]) context.deploy_master @@ -50,10 +50,10 @@ describe 'CycleAnalytics#code', feature: true do -> (context, data) do context.create_commit_referencing_issue(data[:issue]) end]], - end_time_conditions: [["merge request that closes issue is created", - -> (context, data) do - context.create_merge_request_closing_issue(data[:issue]) - end]], + end_time_conditions: [["merge request that closes issue is created", + -> (context, data) do + context.create_merge_request_closing_issue(data[:issue]) + end]], post_fn: -> (context, data) do context.merge_merge_requests_closing_issue(data[:issue]) end) diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index 5ed3d37f2fb..ce6e99bbec9 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } generate_cycle_analytics_spec( phase: :issue, diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb index baf3e3241a1..bd5a6a77b7a 100644 --- a/spec/models/cycle_analytics/plan_spec.rb +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } generate_cycle_analytics_spec( phase: :plan, diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 21b9c6e7150..653e203b491 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } generate_cycle_analytics_spec( phase: :production, diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 158621d59a4..219cd4c0212 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } generate_cycle_analytics_spec( phase: :review, diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index dad653964b7..8dffb6b8fe1 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } generate_cycle_analytics_spec( phase: :staging, diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb index 725bc68b25f..1a54c57a278 100644 --- a/spec/models/cycle_analytics/summary_spec.rb +++ b/spec/models/cycle_analytics/summary_spec.rb @@ -4,7 +4,7 @@ describe CycleAnalytics::Summary, models: true do let(:project) { create(:project) } let(:from) { Time.now } let(:user) { create(:user, :admin) } - subject { described_class.new(project, user, from: from) } + subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } describe "#new_issues" do it "finds the number of issues created after the 'from date'" do diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index 2313724e8f3..ac1304beca8 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } generate_cycle_analytics_spec( phase: :test, diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index 8e19a6c92e2..ab624161616 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -1,8 +1,13 @@ +class CycleAnalyticsTest < CycleAnalytics + def method_missing(method_sym, *arguments, &block) + classify_stage(method_sym).new(project: @project, options: @options, stage: method_sym).median + end +end + # rubocop:disable Metrics/AbcSize # Note: The ABC size is large here because we have a method generating test cases with # multiple nested contexts. This shouldn't count as a violation. - module CycleAnalyticsHelpers module TestGeneration # Generate the most common set of specs that all cycle analytics phases need to have. -- cgit v1.2.1 From a67311cb4c9f54af43d300fde5240f9a370193d1 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 23 Nov 2016 11:28:28 +0100 Subject: Fix other spec failures --- app/controllers/concerns/cycle_analytics_params.rb | 4 +- .../projects/cycle_analytics/events_controller.rb | 26 ++-- .../projects/cycle_analytics_controller.rb | 4 +- app/models/cycle_analytics.rb | 11 +- lib/gitlab/cycle_analytics/base_event.rb | 3 +- lib/gitlab/cycle_analytics/base_stage.rb | 2 +- lib/gitlab/cycle_analytics/events.rb | 38 ------ lib/gitlab/cycle_analytics/stage_summary.rb | 22 ++++ lib/gitlab/cycle_analytics/summary.rb | 24 ---- lib/gitlab/cycle_analytics/summary/base.rb | 2 +- spec/lib/gitlab/cycle_analytics/code_event_spec.rb | 2 + spec/lib/gitlab/cycle_analytics/events_spec.rb | 137 +++++++++++---------- .../lib/gitlab/cycle_analytics/issue_event_spec.rb | 2 + spec/lib/gitlab/cycle_analytics/plan_event_spec.rb | 2 + .../cycle_analytics/production_event_spec.rb | 2 + .../gitlab/cycle_analytics/review_event_spec.rb | 2 + .../gitlab/cycle_analytics/shared_event_spec.rb | 8 +- .../gitlab/cycle_analytics/staging_event_spec.rb | 2 + spec/lib/gitlab/cycle_analytics/test_event_spec.rb | 2 + spec/models/cycle_analytics/summary_spec.rb | 2 +- .../analytics_summary_serializer_spec.rb | 4 +- 21 files changed, 149 insertions(+), 152 deletions(-) delete mode 100644 lib/gitlab/cycle_analytics/events.rb create mode 100644 lib/gitlab/cycle_analytics/stage_summary.rb delete mode 100644 lib/gitlab/cycle_analytics/summary.rb diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb index 91456561a17..52e06f4945a 100644 --- a/app/controllers/concerns/cycle_analytics_params.rb +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -1,8 +1,8 @@ module CycleAnalyticsParams extend ActiveSupport::Concern - def options - @options ||= { from: start_date(events_params), current_user: current_user } + def options(params) + @options ||= { from: start_date(params), current_user: current_user } end def start_date(params) diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index 5e9524b15db..e571e1dfce2 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -9,46 +9,46 @@ module Projects before_action :authorize_read_merge_request!, only: [:code, :review] def issue - render_events(events.issue_events) + render_events(cycle_analytics.events_for(:issue)) end def plan - render_events(events.plan_events) + render_events(cycle_analytics.events_for(:plan)) end def code - render_events(events.code_events) + render_events(cycle_analytics.events_for(:code)) end def test - options[:branch] = events_params[:branch_name] + options(events_params)[:branch] = events_params[:branch_name] - render_events(events.test_events) + render_events(cycle_analytics.events_for(:test)) end def review - render_events(events.review_events) + render_events(cycle_analytics.events_for(:review)) end def staging - render_events(events.staging_events) + render_events(cycle_analytics.events_for(:staging)) end def production - render_events(events.production_events) + render_events(cycle_analytics.events_for(:production)) end private - - def render_events(events_list) + + def render_events(events) respond_to do |format| format.html - format.json { render json: { events: events_list } } + format.json { render json: { events: events } } end end - def events - @events ||= Gitlab::CycleAnalytics::Events.new(project: project, options: options) + def cycle_analytics + @cycle_analytics ||= ::CycleAnalytics.new(project, options: options(events_params)) end def events_params diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index 93dbe2819e7..cf53d0a1919 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -6,7 +6,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController before_action :authorize_read_cycle_analytics! def show - @cycle_analytics = ::CycleAnalytics.new(@project, options: options) + @cycle_analytics = ::CycleAnalytics.new(@project, options: options(cycle_analytics_params)) @cycle_analytics_no_data = @cycle_analytics.no_stats? @@ -21,7 +21,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController def cycle_analytics_params return {} unless params[:cycle_analytics].present? - { start_date: params[:cycle_analytics][:start_date] } + params[:cycle_analytics].slice(:start_date) end def cycle_analytics_json diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index 00e9f7c7d5c..5bcc6fa1954 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -7,7 +7,7 @@ class CycleAnalytics end def summary - @summary ||= Gitlab::CycleAnalytics::Summary.new(@project, from: @options[:from]).data + @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project, from: @options[:from]).data end def stats @@ -15,23 +15,26 @@ class CycleAnalytics end def no_stats? - stats.map(&:value).compact.empty? + stats.map { |hash| hash[:value] }.compact.empty? end def permissions(user:) Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) end + def events_for(stage) + classify_stage(stage).new(project: @project, options: @options, stage: stage).events + end + private def stats_per_stage STAGES.map do |stage_name| - classify_stage(method_sym).new(project: @project, options: @options, stage: stage_name).median_data + classify_stage(stage_name).new(project: @project, options: @options, stage: stage_name).median_data end end def classify_stage(stage_name) "Gitlab::CycleAnalytics::#{stage_name.to_s.capitalize}Stage".constantize end - end diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb index c87841c119a..d540cb6549c 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event.rb @@ -5,10 +5,11 @@ module Gitlab attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query - def initialize(fetcher:, stage:) + def initialize(fetcher:, stage:, options:) @query = EventsQuery.new(fetcher: fetcher) @project = fetcher.project @stage = stage + @options = options end def fetch diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index 27971bfc093..162ebf18c77 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -13,7 +13,7 @@ module Gitlab end def events - event_class.new(fetcher: @fetcher, stage: @stage).fetch + event_class.new(fetcher: @fetcher, stage: @stage, options: @options).fetch end def median_data diff --git a/lib/gitlab/cycle_analytics/events.rb b/lib/gitlab/cycle_analytics/events.rb deleted file mode 100644 index 2d703d76cbb..00000000000 --- a/lib/gitlab/cycle_analytics/events.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Gitlab - module CycleAnalytics - class Events - def initialize(project:, options:) - @project = project - @options = options - end - - def issue_events - IssueEvent.new(project: @project, options: @options).fetch - end - - def plan_events - PlanEvent.new(project: @project, options: @options).fetch - end - - def code_events - CodeEvent.new(project: @project, options: @options).fetch - end - - def test_events - TestEvent.new(project: @project, options: @options).fetch - end - - def review_events - ReviewEvent.new(project: @project, options: @options).fetch - end - - def staging_events - StagingEvent.new(project: @project, options: @options).fetch - end - - def production_events - ProductionEvent.new(project: @project, options: @options).fetch - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb new file mode 100644 index 00000000000..dd9e4ac2813 --- /dev/null +++ b/lib/gitlab/cycle_analytics/stage_summary.rb @@ -0,0 +1,22 @@ +module Gitlab + module CycleAnalytics + class StageSummary + def initialize(project, from:) + @project = project + @from = from + end + + def data + [serialize(Summary::Issue.new(project: @project, from: @from)), + serialize(Summary::Commit.new(project: @project, from: @from)), + serialize(Summary::Deploy.new(project: @project, from: @from))] + end + + private + + def serialize(summary_object) + AnalyticsSummarySerializer.new.represent(summary_object).as_json + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary.rb b/lib/gitlab/cycle_analytics/summary.rb deleted file mode 100644 index 5f0103c9d5a..00000000000 --- a/lib/gitlab/cycle_analytics/summary.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Gitlab - module CycleAnalytics - module Summary - extend self - - def initialize(project, from:) - @project = project - @from = from - end - - def data - [serialize(Summary::Issue.new(project: @project, from: @from)), - serialize(Summary::Commit.new(project: @project, from: @from)), - serialize(Summary::Deploy.new(project: @project, from: @from))] - end - - private - - def serialize(summary_object) - AnalyticsSummarySerializer.new.represent(summary_object).as_json - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/summary/base.rb b/lib/gitlab/cycle_analytics/summary/base.rb index 1bc4ff00b99..43fa3795e5c 100644 --- a/lib/gitlab/cycle_analytics/summary/base.rb +++ b/lib/gitlab/cycle_analytics/summary/base.rb @@ -8,7 +8,7 @@ module Gitlab end def title - self.name + self.class.name.demodulize end def value diff --git a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb index 43f42d1bde8..0673906e678 100644 --- a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::CodeEvent do + let(:stage_name) { :code } + it_behaves_like 'default query config' do it 'does not have the default order' do expect(event.order).not_to eq(event.start_time_attrs) diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 6062e7af4f5..1258f4ed450 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' -describe Gitlab::CycleAnalytics::Events do +describe 'cycle analytics events' do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } - subject { described_class.new(project: project, options: { from: from_date, current_user: user }) } + let(:events) do + CycleAnalytics.new(project, options: { from: from_date, current_user: user }).events_for(stage) + end before do allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context]) @@ -15,104 +17,112 @@ describe Gitlab::CycleAnalytics::Events do end describe '#issue_events' do + let(:stage) { :issue } + it 'has the total time' do - expect(subject.issue_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.issue_events.first[:title]).to eq(context.title) + expect(events.first[:title]).to eq(context.title) end it 'has the URL' do - expect(subject.issue_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has an iid' do - expect(subject.issue_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.issue_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.issue_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.issue_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.issue_events.first[:author][:name]).to eq(context.author.name) + expect(events.first[:author][:name]).to eq(context.author.name) end end describe '#plan_events' do + let(:stage) { :plan } + it 'has a title' do - expect(subject.plan_events.first[:title]).not_to be_nil + expect(events.first[:title]).not_to be_nil end it 'has a sha short ID' do - expect(subject.plan_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the URL' do - expect(subject.plan_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the total time' do - expect(subject.plan_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it "has the author's URL" do - expect(subject.plan_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.plan_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.plan_events.first[:author][:name]).not_to be_nil + expect(events.first[:author][:name]).not_to be_nil end end describe '#code_events' do + let(:stage) { :code } + before do create_commit_referencing_issue(context) end it 'has the total time' do - expect(subject.code_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.code_events.first[:title]).to eq('Awesome merge_request') + expect(events.first[:title]).to eq('Awesome merge_request') end it 'has an iid' do - expect(subject.code_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.code_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.code_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.code_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.code_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#test_events' do + let(:stage) { :test } + let(:merge_request) { MergeRequest.first } let!(:pipeline) do create(:ci_pipeline, @@ -130,83 +140,85 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the name' do - expect(subject.test_events.first[:name]).not_to be_nil + expect(events.first[:name]).not_to be_nil end it 'has the ID' do - expect(subject.test_events.first[:id]).not_to be_nil + expect(events.first[:id]).not_to be_nil end it 'has the URL' do - expect(subject.test_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has the branch name' do - expect(subject.test_events.first[:branch]).not_to be_nil + expect(events.first[:branch]).not_to be_nil end it 'has the branch URL' do - expect(subject.test_events.first[:branch][:url]).not_to be_nil + expect(events.first[:branch][:url]).not_to be_nil end it 'has the short SHA' do - expect(subject.test_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the commit URL' do - expect(subject.test_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the date' do - expect(subject.test_events.first[:date]).not_to be_nil + expect(events.first[:date]).not_to be_nil end it 'has the total time' do - expect(subject.test_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end end describe '#review_events' do + let(:stage) { :review } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } it 'has the total time' do - expect(subject.review_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.review_events.first[:title]).to eq('Awesome merge_request') + expect(events.first[:title]).to eq('Awesome merge_request') end it 'has an iid' do - expect(subject.review_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has the URL' do - expect(subject.review_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has a state' do - expect(subject.review_events.first[:state]).not_to be_nil + expect(events.first[:state]).not_to be_nil end it 'has a created_at timestamp' do - expect(subject.review_events.first[:created_at]).not_to be_nil + expect(events.first[:created_at]).not_to be_nil end it "has the author's URL" do - expect(subject.review_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.review_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.review_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#staging_events' do + let(:stage) { :staging } let(:merge_request) { MergeRequest.first } let!(:pipeline) do create(:ci_pipeline, @@ -227,55 +239,56 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the name' do - expect(subject.staging_events.first[:name]).not_to be_nil + expect(events.first[:name]).not_to be_nil end it 'has the ID' do - expect(subject.staging_events.first[:id]).not_to be_nil + expect(events.first[:id]).not_to be_nil end it 'has the URL' do - expect(subject.staging_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has the branch name' do - expect(subject.staging_events.first[:branch]).not_to be_nil + expect(events.first[:branch]).not_to be_nil end it 'has the branch URL' do - expect(subject.staging_events.first[:branch][:url]).not_to be_nil + expect(events.first[:branch][:url]).not_to be_nil end it 'has the short SHA' do - expect(subject.staging_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the commit URL' do - expect(subject.staging_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the date' do - expect(subject.staging_events.first[:date]).not_to be_nil + expect(events.first[:date]).not_to be_nil end it 'has the total time' do - expect(subject.staging_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it "has the author's URL" do - expect(subject.staging_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.staging_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.staging_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#production_events' do + let(:stage) { :production } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } before do @@ -284,35 +297,35 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the total time' do - expect(subject.production_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.production_events.first[:title]).to eq(context.title) + expect(events.first[:title]).to eq(context.title) end it 'has the URL' do - expect(subject.production_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has an iid' do - expect(subject.production_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.production_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.production_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.production_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.production_events.first[:author][:name]).to eq(context.author.name) + expect(events.first[:author][:name]).to eq(context.author.name) end end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb index 1c5c308da7d..7967d3727db 100644 --- a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::IssueEvent do + let(:stage_name) { :issue } + it_behaves_like 'default query config' do it 'has the default order' do expect(event.order).to eq(event.start_time_attrs) diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb index 4a5604115ec..5c4b8b343bd 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::PlanEvent do + let(:stage_name) { :plan } + it_behaves_like 'default query config' do it 'has the default order' do expect(event.order).to eq(event.start_time_attrs) diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb index ac17e3b4287..99ed9a0ab5c 100644 --- a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::ProductionEvent do + let(:stage_name) { :production } + it_behaves_like 'default query config' do it 'has the default order' do expect(event.order).to eq(event.start_time_attrs) diff --git a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb index 1ff53aa0227..efc40d4ca4a 100644 --- a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::ReviewEvent do + let(:stage_name) { :review } + it_behaves_like 'default query config' do it 'has the default order' do expect(event.order).to eq(event.start_time_attrs) diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb index 7019e4c3351..0b0ea662b74 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -1,7 +1,13 @@ require 'spec_helper' shared_examples 'default query config' do - let(:event) { described_class.new(project: double, options: {}) } + let(:fetcher) do + Gitlab::CycleAnalytics::MetricsFetcher.new(project: create(:empty_project), + from: 1.day.ago, + branch: nil) + end + + let(:event) { described_class.new(fetcher: fetcher, stage: stage_name, options: {}) } it 'has the start attributes' do expect(event.start_time_attrs).not_to be_nil diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb index 4862d4765f2..b7ab477067c 100644 --- a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::StagingEvent do + let(:stage_name) { :staging } + it_behaves_like 'default query config' do it 'does not have the default order' do expect(event.order).not_to eq(event.start_time_attrs) diff --git a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb index e249db69fc6..a4fc8963e5b 100644 --- a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::TestEvent do + let(:stage_name) { :test } + it_behaves_like 'default query config' do it 'does not have the default order' do expect(event.order).not_to eq(event.start_time_attrs) diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb index 1a54c57a278..a8c1c4b9c5e 100644 --- a/spec/models/cycle_analytics/summary_spec.rb +++ b/spec/models/cycle_analytics/summary_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CycleAnalytics::Summary, models: true do +describe Gitlab::CycleAnalytics::StageSummary, models: true do let(:project) { create(:project) } let(:from) { Time.now } let(:user) { create(:user, :admin) } diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb index e08e3f88710..fe551734bc1 100644 --- a/spec/serializers/analytics_summary_serializer_spec.rb +++ b/spec/serializers/analytics_summary_serializer_spec.rb @@ -8,10 +8,10 @@ describe AnalyticsSummarySerializer do let(:json) { serializer.as_json } let(:project) { create(:empty_project) } - let(:resource) { Gitlab::CycleAnalytics::Summary::Issue.new(project: double, from: 1.day.ago) } + let(:resource) { Gitlab::CycleAnalytics::StageSummary::Issue.new(project: double, from: 1.day.ago) } before do - allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue).to receive(:value).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::StageSummary::Issue).to receive(:value).and_return(1.12) end it 'it generates payload for single object' do -- cgit v1.2.1 From e4e313fccab6816fb2e52ebdf83807fba4a52051 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 23 Nov 2016 13:04:06 +0100 Subject: Fix other spec failures --- .../gitlab/cycle_analytics/stage_summary_spec.rb | 59 ++++++++++++++++++++++ spec/models/cycle_analytics/summary_spec.rb | 59 ---------------------- .../analytics_summary_serializer_spec.rb | 4 +- 3 files changed, 61 insertions(+), 61 deletions(-) create mode 100644 spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb delete mode 100644 spec/models/cycle_analytics/summary_spec.rb diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb new file mode 100644 index 00000000000..77dbf1c79a5 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Gitlab::CycleAnalytics::StageSummary, models: true do + let(:project) { create(:project) } + let(:from) { Time.now } + let(:user) { create(:user, :admin) } + subject { described_class.new(project, from: Time.now).data } + + describe "#new_issues" do + it "finds the number of issues created after the 'from date'" do + Timecop.freeze(5.days.ago) { create(:issue, project: project) } + Timecop.freeze(5.days.from_now) { create(:issue, project: project) } + + expect(subject.first[:value]).to eq(1) + end + + it "doesn't find issues from other projects" do + Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) } + + expect(subject.first[:value]).to eq(0) + end + end + + describe "#commits" do + it "finds the number of commits created after the 'from date'" do + Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') } + Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') } + + expect(subject.second[:value]).to eq(1) + end + + it "doesn't find commits from other projects" do + Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') } + + expect(subject.second[:value]).to eq(0) + end + + it "finds a large (> 100) snumber of commits if present" do + Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) } + + expect(subject.second[:value]).to eq(100) + end + end + + describe "#deploys" do + it "finds the number of deploys made created after the 'from date'" do + Timecop.freeze(5.days.ago) { create(:deployment, project: project) } + Timecop.freeze(5.days.from_now) { create(:deployment, project: project) } + + expect(subject.third[:value]).to eq(1) + end + + it "doesn't find commits from other projects" do + Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) } + + expect(subject.third[:value]).to eq(0) + end + end +end diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb deleted file mode 100644 index a8c1c4b9c5e..00000000000 --- a/spec/models/cycle_analytics/summary_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'spec_helper' - -describe Gitlab::CycleAnalytics::StageSummary, models: true do - let(:project) { create(:project) } - let(:from) { Time.now } - let(:user) { create(:user, :admin) } - subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } - - describe "#new_issues" do - it "finds the number of issues created after the 'from date'" do - Timecop.freeze(5.days.ago) { create(:issue, project: project) } - Timecop.freeze(5.days.from_now) { create(:issue, project: project) } - - expect(subject.new_issues).to eq(1) - end - - it "doesn't find issues from other projects" do - Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) } - - expect(subject.new_issues).to eq(0) - end - end - - describe "#commits" do - it "finds the number of commits created after the 'from date'" do - Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') } - Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') } - - expect(subject.commits).to eq(1) - end - - it "doesn't find commits from other projects" do - Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') } - - expect(subject.commits).to eq(0) - end - - it "finds a large (> 100) snumber of commits if present" do - Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) } - - expect(subject.commits).to eq(100) - end - end - - describe "#deploys" do - it "finds the number of deploys made created after the 'from date'" do - Timecop.freeze(5.days.ago) { create(:deployment, project: project) } - Timecop.freeze(5.days.from_now) { create(:deployment, project: project) } - - expect(subject.deploys).to eq(1) - end - - it "doesn't find commits from other projects" do - Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) } - - expect(subject.deploys).to eq(0) - end - end -end diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb index fe551734bc1..e08e3f88710 100644 --- a/spec/serializers/analytics_summary_serializer_spec.rb +++ b/spec/serializers/analytics_summary_serializer_spec.rb @@ -8,10 +8,10 @@ describe AnalyticsSummarySerializer do let(:json) { serializer.as_json } let(:project) { create(:empty_project) } - let(:resource) { Gitlab::CycleAnalytics::StageSummary::Issue.new(project: double, from: 1.day.ago) } + let(:resource) { Gitlab::CycleAnalytics::Summary::Issue.new(project: double, from: 1.day.ago) } before do - allow_any_instance_of(Gitlab::CycleAnalytics::StageSummary::Issue).to receive(:value).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue).to receive(:value).and_return(1.12) end it 'it generates payload for single object' do -- cgit v1.2.1 From 8183e848648bc737e4a09f76f4f55ee1cf106b26 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 23 Nov 2016 15:35:47 +0100 Subject: fix tricky test failure to do with private method --- spec/support/cycle_analytics_helpers/test_generation.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index ab624161616..dc866b11429 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -56,7 +56,7 @@ module CycleAnalyticsHelpers end median_time_difference = time_differences.sort[2] - expect(subject.send(phase)).to be_within(5).of(median_time_difference) + expect(subject.public_send(phase)).to be_within(5).of(median_time_difference) end context "when the data belongs to another project" do @@ -88,7 +88,7 @@ module CycleAnalyticsHelpers # Turn off the stub before checking assertions allow(self).to receive(:project).and_call_original - expect(subject.send(phase)).to be_nil + expect(subject.public_send(phase)).to be_nil end end @@ -111,7 +111,7 @@ module CycleAnalyticsHelpers Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn - expect(subject.send(phase)).to be_nil + expect(subject.public_send(phase)).to be_nil end end end @@ -131,7 +131,7 @@ module CycleAnalyticsHelpers Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn end - expect(subject.send(phase)).to be_nil + expect(subject.public_send(phase)).to be_nil end end end @@ -150,7 +150,7 @@ module CycleAnalyticsHelpers post_fn[self, data] if post_fn end - expect(subject.send(phase)).to be_nil + expect(subject.public_send(phase)).to be_nil end end end @@ -158,7 +158,7 @@ module CycleAnalyticsHelpers context "when none of the start / end conditions are matched" do it "returns nil" do - expect(subject.send(phase)).to be_nil + expect(subject.public_send(phase)).to be_nil end end end -- cgit v1.2.1 From b8056669849729cab5700466a7fae6dc6b2743b2 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 1 Dec 2016 11:21:24 +0100 Subject: refactor cycle analytics - updated based on MR feedback --- .../projects/cycle_analytics/events_controller.rb | 16 ++++++++-------- .../projects/cycle_analytics_controller.rb | 4 ++-- app/models/cycle_analytics.rb | 12 ++++-------- app/serializers/analytics_stage_entity.rb | 4 +--- lib/gitlab/cycle_analytics/base_event.rb | 10 +++++++--- lib/gitlab/cycle_analytics/base_stage.rb | 19 +++++++++++++------ lib/gitlab/cycle_analytics/class_name_util.rb | 13 +++++++++++++ lib/gitlab/cycle_analytics/code_stage.rb | 12 +++++------- lib/gitlab/cycle_analytics/event.rb | 9 +++++++++ lib/gitlab/cycle_analytics/issue_stage.rb | 14 ++++++-------- lib/gitlab/cycle_analytics/metrics_fetcher.rb | 2 +- lib/gitlab/cycle_analytics/plan_stage.rb | 14 ++++++-------- lib/gitlab/cycle_analytics/production_stage.rb | 12 +++++------- lib/gitlab/cycle_analytics/review_stage.rb | 12 +++++------- lib/gitlab/cycle_analytics/stage.rb | 9 +++++++++ lib/gitlab/cycle_analytics/staging_stage.rb | 12 +++++------- lib/gitlab/cycle_analytics/test_stage.rb | 12 +++++------- spec/lib/gitlab/cycle_analytics/events_spec.rb | 2 +- spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb | 2 +- spec/serializers/analytics_stage_serializer_spec.rb | 2 +- 20 files changed, 107 insertions(+), 85 deletions(-) create mode 100644 lib/gitlab/cycle_analytics/class_name_util.rb create mode 100644 lib/gitlab/cycle_analytics/event.rb create mode 100644 lib/gitlab/cycle_analytics/stage.rb diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index e571e1dfce2..d4969c66467 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -9,33 +9,33 @@ module Projects before_action :authorize_read_merge_request!, only: [:code, :review] def issue - render_events(cycle_analytics.events_for(:issue)) + render_events(cycle_analytics[:issue].events) end def plan - render_events(cycle_analytics.events_for(:plan)) + render_events(cycle_analytics[:plan].events) end def code - render_events(cycle_analytics.events_for(:code)) + render_events(cycle_analytics[:code].events) end def test options(events_params)[:branch] = events_params[:branch_name] - render_events(cycle_analytics.events_for(:test)) + render_events(cycle_analytics[:test].events) end def review - render_events(cycle_analytics.events_for(:review)) + render_events(cycle_analytics[:review].events) end def staging - render_events(cycle_analytics.events_for(:staging)) + render_events(cycle_analytics[:staging].events) end def production - render_events(cycle_analytics.events_for(:production)) + render_events(cycle_analytics[:production].events) end private @@ -54,7 +54,7 @@ module Projects def events_params return {} unless params[:events].present? - params[:events].slice(:start_date, :branch_name) + params[:events].permit(:start_date, :branch_name) end end end diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index cf53d0a1919..88ac3ad046b 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -6,7 +6,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController before_action :authorize_read_cycle_analytics! def show - @cycle_analytics = ::CycleAnalytics.new(@project, options: options(cycle_analytics_params)) + @cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params)) @cycle_analytics_no_data = @cycle_analytics.no_stats? @@ -21,7 +21,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController def cycle_analytics_params return {} unless params[:cycle_analytics].present? - params[:cycle_analytics].slice(:start_date) + params[:cycle_analytics].permit(:start_date) end def cycle_analytics_json diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index 5bcc6fa1954..c6862c9733d 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -1,7 +1,7 @@ class CycleAnalytics STAGES = %i[issue plan code test review staging production].freeze - def initialize(project, options:) + def initialize(project, options) @project = project @options = options end @@ -22,19 +22,15 @@ class CycleAnalytics Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) end - def events_for(stage) - classify_stage(stage).new(project: @project, options: @options, stage: stage).events + def [](stage_name) + Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options) end private def stats_per_stage STAGES.map do |stage_name| - classify_stage(stage_name).new(project: @project, options: @options, stage: stage_name).median_data + Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options).median_data end end - - def classify_stage(stage_name) - "Gitlab::CycleAnalytics::#{stage_name.to_s.capitalize}Stage".constantize - end end diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb index d454a4937f4..a559d0850c4 100644 --- a/app/serializers/analytics_stage_entity.rb +++ b/app/serializers/analytics_stage_entity.rb @@ -1,9 +1,7 @@ class AnalyticsStageEntity < Grape::Entity include EntityDateHelper - expose :stage, as: :title do |object| - object.stage.to_s.capitalize - end + expose :title expose :description expose :median, as: :value do |stage| diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb index d540cb6549c..eac807af037 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event.rb @@ -2,13 +2,13 @@ module Gitlab module CycleAnalytics class BaseEvent include MetricsTables + include ClassNameUtil - attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query + attr_reader :start_time_attrs, :end_time_attrs, :projections, :query - def initialize(fetcher:, stage:, options:) + def initialize(fetcher:, options:) @query = EventsQuery.new(fetcher: fetcher) @project = fetcher.project - @stage = stage @options = options end @@ -26,6 +26,10 @@ module Gitlab @order || @start_time_attrs end + def stage + class_name_for('Event') + end + private def update_author! diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index 162ebf18c77..551318f536a 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -1,29 +1,36 @@ module Gitlab module CycleAnalytics class BaseStage - attr_reader :stage, :description + include ClassNameUtil - def initialize(project:, options:, stage:) + def initialize(project:, options:) @project = project @options = options @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: options[:from], branch: options[:branch]) - @stage = stage end def events - event_class.new(fetcher: @fetcher, stage: @stage, options: @options).fetch + Gitlab::CycleAnalytics::Event[stage].new(fetcher: @fetcher, options: @options).fetch end def median_data AnalyticsStageSerializer.new.represent(self).as_json end + def title + stage.to_s.capitalize + end + + def median + raise NotImplementedError.new("Expected #{self.name} to implement median") + end + private - def event_class - "Gitlab::CycleAnalytics::#{@stage.to_s.capitalize}Event".constantize + def stage + class_name_for('Stage') end end end diff --git a/lib/gitlab/cycle_analytics/class_name_util.rb b/lib/gitlab/cycle_analytics/class_name_util.rb new file mode 100644 index 00000000000..aac8d888077 --- /dev/null +++ b/lib/gitlab/cycle_analytics/class_name_util.rb @@ -0,0 +1,13 @@ +module Gitlab + module CycleAnalytics + module ClassNameUtil + def class_name_for(type) + class_name.split(type).first.to_sym + end + + def class_name + self.class.name.demodulize + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index f72989c9a72..778ce7b5435 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -1,16 +1,14 @@ module Gitlab module CycleAnalytics class CodeStage < BaseStage - def initialize(*args) - super(*args) - - @description = "Time until first merge request" + def description + "Time until first merge request" end def median - @fetcher.calculate_metric(:code, - Issue::Metrics.arel_table[:first_mentioned_in_commit_at], - MergeRequest.arel_table[:created_at]) + @fetcher.median(:code, + Issue::Metrics.arel_table[:first_mentioned_in_commit_at], + MergeRequest.arel_table[:created_at]) end end end diff --git a/lib/gitlab/cycle_analytics/event.rb b/lib/gitlab/cycle_analytics/event.rb new file mode 100644 index 00000000000..62fac89a0d5 --- /dev/null +++ b/lib/gitlab/cycle_analytics/event.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module Event + def self.[](stage_name) + const_get("::Gitlab::CycleAnalytics::#{stage_name.to_s.camelize}Event") + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index a2ada238cd2..c317872fb1d 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -1,17 +1,15 @@ module Gitlab module CycleAnalytics class IssueStage < BaseStage - def initialize(*args) - super(*args) - - @description = "Time before an issue gets scheduled" + def description + "Time before an issue gets scheduled" end def median - @fetcher.calculate_metric(:issue, - Issue.arel_table[:created_at], - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]]) + @fetcher.median(:issue, + Issue.arel_table[:created_at], + [Issue::Metrics.arel_table[:first_associated_with_milestone_at], + Issue::Metrics.arel_table[:first_added_to_board_at]]) end end end diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb index 51835bbde24..bd68a0980ca 100644 --- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb +++ b/lib/gitlab/cycle_analytics/metrics_fetcher.rb @@ -15,7 +15,7 @@ module Gitlab @branch = branch end - def calculate_metric(name, start_time_attrs, end_time_attrs) + def median(name, start_time_attrs, end_time_attrs) cte_table = Arel::Table.new("cte_table_for_#{name}") # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index c836068c4ef..5e6dd30d9e3 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -1,17 +1,15 @@ module Gitlab module CycleAnalytics class PlanStage < BaseStage - def initialize(*args) - super(*args) - - @description = "Time before an issue starts implementation" + def description + "Time before an issue starts implementation" end def median - @fetcher.calculate_metric(:plan, - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]], - Issue::Metrics.arel_table[:first_mentioned_in_commit_at]) + @fetcher.median(:plan, + [Issue::Metrics.arel_table[:first_associated_with_milestone_at], + Issue::Metrics.arel_table[:first_added_to_board_at]], + Issue::Metrics.arel_table[:first_mentioned_in_commit_at]) end end end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb index d46d37e1acc..acd2f7b2b3b 100644 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -1,16 +1,14 @@ module Gitlab module CycleAnalytics class ProductionStage < BaseStage - def initialize(*args) - super(*args) - - @description = "From issue creation until deploy to production" + def description + "From issue creation until deploy to production" end def median - @fetcher.calculate_metric(:production, - Issue.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + @fetcher.median(:production, + Issue.arel_table[:created_at], + MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) end end end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index 4159ba5d70d..c7b5e34e16a 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -1,16 +1,14 @@ module Gitlab module CycleAnalytics class ReviewStage < BaseStage - def initialize(*args) - super(*args) - - @description = "Time between merge request creation and merge/close" + def description + "Time between merge request creation and merge/close" end def median - @fetcher.calculate_metric(:review, - MergeRequest.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:merged_at]) + @fetcher.median(:review, + MergeRequest.arel_table[:created_at], + MergeRequest::Metrics.arel_table[:merged_at]) end end end diff --git a/lib/gitlab/cycle_analytics/stage.rb b/lib/gitlab/cycle_analytics/stage.rb new file mode 100644 index 00000000000..acf746db6cd --- /dev/null +++ b/lib/gitlab/cycle_analytics/stage.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module Stage + def self.[](stage_name) + const_get("::Gitlab::CycleAnalytics::#{stage_name.to_s.camelize}Stage") + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index cb4398f15ac..b715a9453c7 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -1,16 +1,14 @@ module Gitlab module CycleAnalytics class StagingStage < BaseStage - def initialize(*args) - super(*args) - - @description = "From merge request merge until deploy to production" + def description + "From merge request merge until deploy to production" end def median - @fetcher.calculate_metric(:staging, - MergeRequest::Metrics.arel_table[:merged_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + @fetcher.median(:staging, + MergeRequest::Metrics.arel_table[:merged_at], + MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) end end end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index 3ab93bebd87..58f72bb405e 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -1,16 +1,14 @@ module Gitlab module CycleAnalytics class TestStage < BaseStage - def initialize(*args) - super(*args) - - @description = "Total test time for all commits/merges" + def description + "Total test time for all commits/merges" end def median - @fetcher.calculate_metric(:test, - MergeRequest::Metrics.arel_table[:latest_build_started_at], - MergeRequest::Metrics.arel_table[:latest_build_finished_at]) + @fetcher.median(:test, + MergeRequest::Metrics.arel_table[:latest_build_started_at], + MergeRequest::Metrics.arel_table[:latest_build_finished_at]) end end end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 1258f4ed450..9d2ba481919 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -7,7 +7,7 @@ describe 'cycle analytics events' do let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } let(:events) do - CycleAnalytics.new(project, options: { from: from_date, current_user: user }).events_for(stage) + CycleAnalytics.new(project, { from: from_date, current_user: user })[stage].events end before do diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb index dd1ef4fc129..f4189d3c7fc 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -4,7 +4,7 @@ shared_examples 'base stage' do let(:stage) { described_class.new(project: double, options: {}, stage: stage_name) } before do - allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:calculate_metric).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:median).and_return(1.12) allow_any_instance_of(Gitlab::CycleAnalytics::BaseEvent).to receive(:event_result).and_return({}) end diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb index 0f2d534e714..47c537fcf84 100644 --- a/spec/serializers/analytics_stage_serializer_spec.rb +++ b/spec/serializers/analytics_stage_serializer_spec.rb @@ -10,7 +10,7 @@ describe AnalyticsStageSerializer do let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}, stage: :code) } before do - allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:calculate_metric).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:median).and_return(1.12) allow_any_instance_of(Gitlab::CycleAnalytics::BaseEvent).to receive(:event_result).and_return({}) end -- cgit v1.2.1 From 69ecd951a9e0acbc31f0a4b9b02e8a1981ceff1e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 1 Dec 2016 12:44:35 +0100 Subject: refactor fetcher and fixed specs --- .../projects/cycle_analytics/events_controller.rb | 2 +- lib/gitlab/cycle_analytics/base_event.rb | 4 +-- lib/gitlab/cycle_analytics/events_query.rb | 32 ---------------------- lib/gitlab/cycle_analytics/metrics_fetcher.rb | 15 ++++++++++ lib/gitlab/database/median.rb | 5 ++++ spec/models/cycle_analytics/code_spec.rb | 6 ++-- spec/models/cycle_analytics/issue_spec.rb | 4 +-- spec/models/cycle_analytics/plan_spec.rb | 4 +-- spec/models/cycle_analytics/production_spec.rb | 6 ++-- spec/models/cycle_analytics/review_spec.rb | 4 +-- spec/models/cycle_analytics/staging_spec.rb | 6 ++-- spec/models/cycle_analytics/test_spec.rb | 10 +++---- .../cycle_analytics_helpers/test_generation.rb | 18 ++++-------- 13 files changed, 49 insertions(+), 67 deletions(-) delete mode 100644 lib/gitlab/cycle_analytics/events_query.rb diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index d4969c66467..b69d46f2c41 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -48,7 +48,7 @@ module Projects end def cycle_analytics - @cycle_analytics ||= ::CycleAnalytics.new(project, options: options(events_params)) + @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params)) end def events_params diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb index eac807af037..2ce9c34d8e8 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event.rb @@ -7,7 +7,7 @@ module Gitlab attr_reader :start_time_attrs, :end_time_attrs, :projections, :query def initialize(fetcher:, options:) - @query = EventsQuery.new(fetcher: fetcher) + @fetcher = fetcher @project = fetcher.project @options = options end @@ -39,7 +39,7 @@ module Gitlab end def event_result - @event_result ||= @query.execute(self).to_a + @event_result ||= @fetcher.events(self).to_a end def serialize(_event) diff --git a/lib/gitlab/cycle_analytics/events_query.rb b/lib/gitlab/cycle_analytics/events_query.rb deleted file mode 100644 index e2b79384c9b..00000000000 --- a/lib/gitlab/cycle_analytics/events_query.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Gitlab - module CycleAnalytics - class EventsQuery - def initialize(fetcher:) - @fetcher = fetcher - end - - def execute(stage_class) - @stage_class = stage_class - - ActiveRecord::Base.connection.exec_query(query.to_sql) - end - - private - - def query - base_query = @fetcher.base_query_for(@stage_class.stage) - diff_fn = @fetcher.subtract_datetimes_diff(base_query, @stage_class.start_time_attrs, @stage_class.end_time_attrs) - - @stage_class.custom_query(base_query) - - base_query.project(extract_epoch(diff_fn).as('total_time'), *@stage_class.projections).order(@stage_class.order.desc) - end - - def extract_epoch(arel_attribute) - return arel_attribute unless Gitlab::Database.postgresql? - - Arel.sql(%Q{EXTRACT(EPOCH FROM (#{arel_attribute.to_sql}))}) - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb index bd68a0980ca..0542fbfb38d 100644 --- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb +++ b/lib/gitlab/cycle_analytics/metrics_fetcher.rb @@ -29,6 +29,21 @@ module Gitlab median_datetime(cte_table, interval_query, name) end + def events(stage_class) + ActiveRecord::Base.connection.exec_query(events_query(stage_class).to_sql) + end + + private + + def events_query(stage_class) + base_query = base_query_for(stage_class.stage) + diff_fn = subtract_datetimes_diff(base_query, stage_class.start_time_attrs, stage_class.end_time_attrs) + + stage_class.custom_query(base_query) + + base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *stage_class.projections).order(stage_class.order.desc) + end + # Join table with a row for every pair (where the merge request # closes the given issue) with issue and merge request metrics included. The metrics # are loaded with an inner join, so issues / merge requests without metrics are diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb index 1444d25ebc7..08607c27c09 100644 --- a/lib/gitlab/database/median.rb +++ b/lib/gitlab/database/median.rb @@ -103,6 +103,11 @@ module Gitlab Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")}) end + def extract_diff_epoch(diff) + return diff unless Gitlab::Database.postgresql? + + Arel.sql(%Q{EXTRACT(EPOCH FROM (#{diff.to_sql}))}) + end # Need to cast '0' to an INTERVAL before we can check if the interval is positive def zero_interval Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")]) diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 4838b57e353..70f985afefb 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } + subject { CycleAnalytics.new(project, from: from_date) } context 'with deployment' do generate_cycle_analytics_spec( @@ -37,7 +37,7 @@ describe 'CycleAnalytics#code', feature: true do deploy_master end - expect(subject.code).to be_nil + expect(subject[:code].median).to be_nil end end end @@ -69,7 +69,7 @@ describe 'CycleAnalytics#code', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.code).to be_nil + expect(subject[:code].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index ce6e99bbec9..e4b6a8f4518 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :issue, @@ -42,7 +42,7 @@ describe 'CycleAnalytics#issue', models: true do merge_merge_requests_closing_issue(issue) end - expect(subject.issue).to be_nil + expect(subject[:issue].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb index bd5a6a77b7a..dc5b04852d6 100644 --- a/spec/models/cycle_analytics/plan_spec.rb +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :plan, @@ -44,7 +44,7 @@ describe 'CycleAnalytics#plan', feature: true do create_merge_request_closing_issue(issue, source_branch: branch_name) merge_merge_requests_closing_issue(issue) - expect(subject.issue).to be_nil + expect(subject[:issue].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 653e203b491..5e99188f318 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :production, @@ -35,7 +35,7 @@ describe 'CycleAnalytics#production', feature: true do deploy_master end - expect(subject.production).to be_nil + expect(subject[:production].median).to be_nil end end @@ -48,7 +48,7 @@ describe 'CycleAnalytics#production', feature: true do deploy_master(environment: 'staging') end - expect(subject.production).to be_nil + expect(subject[:production].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 219cd4c0212..45baa5f7006 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :review, @@ -27,7 +27,7 @@ describe 'CycleAnalytics#review', feature: true do MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) end - expect(subject.review).to be_nil + expect(subject[:review].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index 8dffb6b8fe1..77625aad580 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :staging, @@ -45,7 +45,7 @@ describe 'CycleAnalytics#staging', feature: true do deploy_master end - expect(subject.staging).to be_nil + expect(subject[:staging].median).to be_nil end end @@ -58,7 +58,7 @@ describe 'CycleAnalytics#staging', feature: true do deploy_master(environment: 'staging') end - expect(subject.staging).to be_nil + expect(subject[:staging].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index ac1304beca8..27a117d2d76 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalyticsTest.new(project, options: { from: from_date }) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :test, @@ -35,7 +35,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -48,7 +48,7 @@ describe 'CycleAnalytics#test', feature: true do pipeline.succeed! end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -65,7 +65,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -82,7 +82,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end end diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index dc866b11429..35b40d73191 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -1,9 +1,3 @@ -class CycleAnalyticsTest < CycleAnalytics - def method_missing(method_sym, *arguments, &block) - classify_stage(method_sym).new(project: @project, options: @options, stage: method_sym).median - end -end - # rubocop:disable Metrics/AbcSize # Note: The ABC size is large here because we have a method generating test cases with @@ -56,7 +50,7 @@ module CycleAnalyticsHelpers end median_time_difference = time_differences.sort[2] - expect(subject.public_send(phase)).to be_within(5).of(median_time_difference) + expect(subject[phase].median).to be_within(5).of(median_time_difference) end context "when the data belongs to another project" do @@ -88,7 +82,7 @@ module CycleAnalyticsHelpers # Turn off the stub before checking assertions allow(self).to receive(:project).and_call_original - expect(subject.public_send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end @@ -111,7 +105,7 @@ module CycleAnalyticsHelpers Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn - expect(subject.public_send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -131,7 +125,7 @@ module CycleAnalyticsHelpers Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn end - expect(subject.public_send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -150,7 +144,7 @@ module CycleAnalyticsHelpers post_fn[self, data] if post_fn end - expect(subject.public_send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -158,7 +152,7 @@ module CycleAnalyticsHelpers context "when none of the start / end conditions are matched" do it "returns nil" do - expect(subject.public_send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end -- cgit v1.2.1 From 58dddcdfed6212046447de8b6d304ffd463d0350 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 1 Dec 2016 13:28:24 +0100 Subject: few fixes after merge --- app/models/cycle_analytics.rb | 4 +++- lib/gitlab/cycle_analytics/stage_summary.rb | 5 +++-- lib/gitlab/cycle_analytics/summary/issue.rb | 8 +++++++- spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb | 4 ++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index c6862c9733d..57082ae6087 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -7,7 +7,9 @@ class CycleAnalytics end def summary - @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project, from: @options[:from]).data + @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project, + from: @options[:from], + current_user: @options[:current_user]).data end def stats diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb index dd9e4ac2813..b34baf5b081 100644 --- a/lib/gitlab/cycle_analytics/stage_summary.rb +++ b/lib/gitlab/cycle_analytics/stage_summary.rb @@ -1,13 +1,14 @@ module Gitlab module CycleAnalytics class StageSummary - def initialize(project, from:) + def initialize(project, from:, current_user:) @project = project @from = from + @current_user = current_user end def data - [serialize(Summary::Issue.new(project: @project, from: @from)), + [serialize(Summary::Issue.new(project: @project, from: @from, current_user: @current_user)), serialize(Summary::Commit.new(project: @project, from: @from)), serialize(Summary::Deploy.new(project: @project, from: @from))] end diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb index 7d62164aae3..008468f24b9 100644 --- a/lib/gitlab/cycle_analytics/summary/issue.rb +++ b/lib/gitlab/cycle_analytics/summary/issue.rb @@ -2,12 +2,18 @@ module Gitlab module CycleAnalytics module Summary class Issue < Base + def initialize(project:, from:, current_user:) + @project = project + @from = from + @current_user = current_user + end + def title 'New Issue' end def value - @value ||= @project.issues.created_after(@from).count + @value ||= IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count end end end diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb index 77dbf1c79a5..fb6b6c4a8d2 100644 --- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Gitlab::CycleAnalytics::StageSummary, models: true do let(:project) { create(:project) } - let(:from) { Time.now } + let(:from) { 1.day.ago } let(:user) { create(:user, :admin) } - subject { described_class.new(project, from: Time.now).data } + subject { described_class.new(project, from: Time.now, current_user: user).data } describe "#new_issues" do it "finds the number of issues created after the 'from date'" do -- cgit v1.2.1 From daa4f3ded718f4144877b7f0402bd495151c28de Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 1 Dec 2016 15:40:46 +0100 Subject: fix spec failures after merge --- spec/lib/gitlab/cycle_analytics/plan_event_spec.rb | 2 +- spec/lib/gitlab/cycle_analytics/shared_event_spec.rb | 2 +- spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb | 6 +++--- spec/serializers/analytics_stage_serializer_spec.rb | 4 ++-- spec/serializers/analytics_summary_serializer_spec.rb | 9 +++++++-- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb index 5c4b8b343bd..df407e51c64 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::CycleAnalytics::PlanEvent do context 'no commits' do it 'does not blow up if there are no commits' do - allow_any_instance_of(Gitlab::CycleAnalytics::EventsQuery).to receive(:execute).and_return([{}]) + allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:events).and_return([{}]) expect { event.fetch }.not_to raise_error end diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb index 0b0ea662b74..5e1c7531fb5 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -7,7 +7,7 @@ shared_examples 'default query config' do branch: nil) end - let(:event) { described_class.new(fetcher: fetcher, stage: stage_name, options: {}) } + let(:event) { described_class.new(fetcher: fetcher, options: {}) } it 'has the start attributes' do expect(event.start_time_attrs).not_to be_nil diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb index f4189d3c7fc..cfb5dc12ff1 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' shared_examples 'base stage' do - let(:stage) { described_class.new(project: double, options: {}, stage: stage_name) } + let(:stage) { described_class.new(project: double, options: {}) } before do allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:median).and_return(1.12) @@ -20,8 +20,8 @@ shared_examples 'base stage' do expect(stage.median_data[:description]).not_to be_nil end - it 'has the stage' do - expect(stage.stage).to eq(stage_name) + it 'has the title' do + expect(stage.title).to eq(stage_name.to_s.capitalize) end it 'has the events' do diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb index 47c537fcf84..3627a21230f 100644 --- a/spec/serializers/analytics_stage_serializer_spec.rb +++ b/spec/serializers/analytics_stage_serializer_spec.rb @@ -7,7 +7,7 @@ describe AnalyticsStageSerializer do end let(:json) { serializer.as_json } - let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}, stage: :code) } + let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) } before do allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:median).and_return(1.12) @@ -15,7 +15,7 @@ describe AnalyticsStageSerializer do end it 'it generates payload for single object' do - expect(json).to be_an_instance_of Hash + expect(json).to be_kind_of Hash end it 'contains important elements of AnalyticsStage' do diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb index e08e3f88710..7a84c8b0b40 100644 --- a/spec/serializers/analytics_summary_serializer_spec.rb +++ b/spec/serializers/analytics_summary_serializer_spec.rb @@ -8,14 +8,19 @@ describe AnalyticsSummarySerializer do let(:json) { serializer.as_json } let(:project) { create(:empty_project) } - let(:resource) { Gitlab::CycleAnalytics::Summary::Issue.new(project: double, from: 1.day.ago) } + let(:user) { create(:user) } + let(:resource) do + Gitlab::CycleAnalytics::Summary::Issue.new(project: double, + from: 1.day.ago, + current_user: user) + end before do allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue).to receive(:value).and_return(1.12) end it 'it generates payload for single object' do - expect(json).to be_an_instance_of Hash + expect(json).to be_kind_of Hash end it 'contains important elements of AnalyticsStage' do -- cgit v1.2.1 From b214be493d9f179d4a929ee32d94a336da7b38f1 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 2 Dec 2016 17:09:29 +0100 Subject: big refactor based on MR feedback --- app/models/cycle_analytics.rb | 2 +- lib/gitlab/cycle_analytics/base_event.rb | 62 ---------------------- lib/gitlab/cycle_analytics/base_event_fetcher.rb | 58 ++++++++++++++++++++ lib/gitlab/cycle_analytics/base_stage.rb | 13 +++-- lib/gitlab/cycle_analytics/class_name_util.rb | 13 ----- lib/gitlab/cycle_analytics/code_event.rb | 27 ---------- lib/gitlab/cycle_analytics/code_event_fetcher.rb | 25 +++++++++ lib/gitlab/cycle_analytics/code_stage.rb | 17 +++--- lib/gitlab/cycle_analytics/event.rb | 2 +- lib/gitlab/cycle_analytics/issue_event.rb | 26 --------- lib/gitlab/cycle_analytics/issue_event_fetcher.rb | 23 ++++++++ lib/gitlab/cycle_analytics/issue_stage.rb | 19 ++++--- lib/gitlab/cycle_analytics/metrics_fetcher.rb | 25 +++++---- lib/gitlab/cycle_analytics/plan_event.rb | 45 ---------------- lib/gitlab/cycle_analytics/plan_event_fetcher.rb | 42 +++++++++++++++ lib/gitlab/cycle_analytics/plan_stage.rb | 19 ++++--- lib/gitlab/cycle_analytics/production_event.rb | 25 --------- .../cycle_analytics/production_event_fetcher.rb | 23 ++++++++ lib/gitlab/cycle_analytics/production_stage.rb | 17 +++--- lib/gitlab/cycle_analytics/review_event.rb | 24 --------- lib/gitlab/cycle_analytics/review_event_fetcher.rb | 22 ++++++++ lib/gitlab/cycle_analytics/review_stage.rb | 17 +++--- lib/gitlab/cycle_analytics/stage.rb | 2 +- lib/gitlab/cycle_analytics/staging_event.rb | 30 ----------- .../cycle_analytics/staging_event_fetcher.rb | 28 ++++++++++ lib/gitlab/cycle_analytics/staging_stage.rb | 17 +++--- lib/gitlab/cycle_analytics/test_event.rb | 12 ----- lib/gitlab/cycle_analytics/test_event_fetcher.rb | 6 +++ lib/gitlab/cycle_analytics/test_stage.rb | 17 +++--- .../cycle_analytics/code_event_fetcher_spec.rb | 12 +++++ spec/lib/gitlab/cycle_analytics/code_event_spec.rb | 12 ----- .../cycle_analytics/issue_event_fetcher_spec.rb | 12 +++++ .../lib/gitlab/cycle_analytics/issue_event_spec.rb | 12 ----- .../cycle_analytics/plan_event_fetcher_spec.rb | 20 +++++++ spec/lib/gitlab/cycle_analytics/plan_event_spec.rb | 20 ------- .../production_event_fetcher_spec.rb | 12 +++++ .../cycle_analytics/production_event_spec.rb | 12 ----- .../cycle_analytics/review_event_fetcher_spec.rb | 12 +++++ .../gitlab/cycle_analytics/review_event_spec.rb | 12 ----- .../gitlab/cycle_analytics/shared_event_spec.rb | 5 +- .../gitlab/cycle_analytics/shared_stage_spec.rb | 2 +- .../cycle_analytics/staging_event_fetcher_spec.rb | 12 +++++ .../gitlab/cycle_analytics/staging_event_spec.rb | 12 ----- .../cycle_analytics/test_event_fetcher_spec.rb | 12 +++++ spec/lib/gitlab/cycle_analytics/test_event_spec.rb | 12 ----- .../serializers/analytics_stage_serializer_spec.rb | 2 +- 46 files changed, 429 insertions(+), 422 deletions(-) delete mode 100644 lib/gitlab/cycle_analytics/base_event.rb create mode 100644 lib/gitlab/cycle_analytics/base_event_fetcher.rb delete mode 100644 lib/gitlab/cycle_analytics/class_name_util.rb delete mode 100644 lib/gitlab/cycle_analytics/code_event.rb create mode 100644 lib/gitlab/cycle_analytics/code_event_fetcher.rb delete mode 100644 lib/gitlab/cycle_analytics/issue_event.rb create mode 100644 lib/gitlab/cycle_analytics/issue_event_fetcher.rb delete mode 100644 lib/gitlab/cycle_analytics/plan_event.rb create mode 100644 lib/gitlab/cycle_analytics/plan_event_fetcher.rb delete mode 100644 lib/gitlab/cycle_analytics/production_event.rb create mode 100644 lib/gitlab/cycle_analytics/production_event_fetcher.rb delete mode 100644 lib/gitlab/cycle_analytics/review_event.rb create mode 100644 lib/gitlab/cycle_analytics/review_event_fetcher.rb delete mode 100644 lib/gitlab/cycle_analytics/staging_event.rb create mode 100644 lib/gitlab/cycle_analytics/staging_event_fetcher.rb delete mode 100644 lib/gitlab/cycle_analytics/test_event.rb create mode 100644 lib/gitlab/cycle_analytics/test_event_fetcher.rb create mode 100644 spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/code_event_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/issue_event_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/plan_event_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/production_event_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/review_event_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/staging_event_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/test_event_spec.rb diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index 57082ae6087..78aebd828d7 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -32,7 +32,7 @@ class CycleAnalytics def stats_per_stage STAGES.map do |stage_name| - Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options).median_data + self[stage_name].new(project: @project, options: @options).median_data end end end diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb deleted file mode 100644 index 2ce9c34d8e8..00000000000 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ /dev/null @@ -1,62 +0,0 @@ -module Gitlab - module CycleAnalytics - class BaseEvent - include MetricsTables - include ClassNameUtil - - attr_reader :start_time_attrs, :end_time_attrs, :projections, :query - - def initialize(fetcher:, options:) - @fetcher = fetcher - @project = fetcher.project - @options = options - end - - def fetch - update_author! - - event_result.map do |event| - serialize(event) if has_permission?(event['id']) - end.compact - end - - def custom_query(_base_query); end - - def order - @order || @start_time_attrs - end - - def stage - class_name_for('Event') - end - - private - - def update_author! - return unless event_result.any? && event_result.first['author_id'] - - Updater.update!(event_result, from: 'author_id', to: 'author', klass: User) - end - - def event_result - @event_result ||= @fetcher.events(self).to_a - end - - def serialize(_event) - raise NotImplementedError.new("Expected #{self.name} to implement serialize(event)") - end - - def has_permission?(id) - allowed_ids.nil? || allowed_ids.include?(id.to_i) - end - - def allowed_ids - nil - end - - def event_result_ids - event_result.map { |event| event['id'] } - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb new file mode 100644 index 00000000000..0d851f81b1d --- /dev/null +++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb @@ -0,0 +1,58 @@ +module Gitlab + module CycleAnalytics + class BaseEventFetcher + include MetricsTables + + attr_reader :projections, :query, :stage + + def initialize(fetcher:, options:, stage:) + @fetcher = fetcher + @project = fetcher.project + @options = options + @stage = stage + end + + def fetch + update_author! + + event_result.map do |event| + serialize(event) if has_permission?(event['id']) + end.compact + end + + def custom_query(_base_query); end + + def order + @order || @start_time_attrs + end + + private + + def update_author! + return unless event_result.any? && event_result.first['author_id'] + + Updater.update!(event_result, from: 'author_id', to: 'author', klass: User) + end + + def event_result + @event_result ||= @fetcher.events(self).to_a + end + + def serialize(_event) + raise NotImplementedError.new("Expected #{self.name} to implement serialize(event)") + end + + def has_permission?(id) + allowed_ids.nil? || allowed_ids.include?(id.to_i) + end + + def allowed_ids + nil + end + + def event_result_ids + event_result.map { |event| event['id'] } + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index 551318f536a..f3b8bb6e1d3 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -1,18 +1,23 @@ module Gitlab module CycleAnalytics class BaseStage - include ClassNameUtil + attr_accessor :start_time_attrs, :end_time_attrs def initialize(project:, options:) @project = project @options = options @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: options[:from], - branch: options[:branch]) + branch: options[:branch], + stage: self) + end + + def event + @event ||= Gitlab::CycleAnalytics::Event[stage].new(fetcher: @fetcher, options: @options) end def events - Gitlab::CycleAnalytics::Event[stage].new(fetcher: @fetcher, options: @options).fetch + event.fetch end def median_data @@ -24,7 +29,7 @@ module Gitlab end def median - raise NotImplementedError.new("Expected #{self.name} to implement median") + @fetcher.median end private diff --git a/lib/gitlab/cycle_analytics/class_name_util.rb b/lib/gitlab/cycle_analytics/class_name_util.rb deleted file mode 100644 index aac8d888077..00000000000 --- a/lib/gitlab/cycle_analytics/class_name_util.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Gitlab - module CycleAnalytics - module ClassNameUtil - def class_name_for(type) - class_name.split(type).first.to_sym - end - - def class_name - self.class.name.demodulize - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/code_event.rb deleted file mode 100644 index 68251630e08..00000000000 --- a/lib/gitlab/cycle_analytics/code_event.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Gitlab - module CycleAnalytics - class CodeEvent < BaseEvent - include MergeRequestAllowed - - def initialize(*args) - @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] - @end_time_attrs = mr_table[:created_at] - @projections = [mr_table[:title], - mr_table[:iid], - mr_table[:id], - mr_table[:created_at], - mr_table[:state], - mr_table[:author_id]] - @order = mr_table[:created_at] - - super(*args) - end - - private - - def serialize(event) - AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb new file mode 100644 index 00000000000..5245b9ca8fc --- /dev/null +++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb @@ -0,0 +1,25 @@ +module Gitlab + module CycleAnalytics + class CodeEventFetcher < BaseEventFetcher + include MergeRequestAllowed + + def initialize(*args) + @projections = [mr_table[:title], + mr_table[:iid], + mr_table[:id], + mr_table[:created_at], + mr_table[:state], + mr_table[:author_id]] + @order = mr_table[:created_at] + + super(*args) + end + + private + + def serialize(event) + AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index 778ce7b5435..977d0d0210c 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -1,14 +1,19 @@ module Gitlab module CycleAnalytics class CodeStage < BaseStage - def description - "Time until first merge request" + def initialize(*args) + @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] + @end_time_attrs = mr_table[:created_at] + + super(*args) end - def median - @fetcher.median(:code, - Issue::Metrics.arel_table[:first_mentioned_in_commit_at], - MergeRequest.arel_table[:created_at]) + def stage + :code + end + + def description + "Time until first merge request" end end end diff --git a/lib/gitlab/cycle_analytics/event.rb b/lib/gitlab/cycle_analytics/event.rb index 62fac89a0d5..bb3a5722a0f 100644 --- a/lib/gitlab/cycle_analytics/event.rb +++ b/lib/gitlab/cycle_analytics/event.rb @@ -2,7 +2,7 @@ module Gitlab module CycleAnalytics module Event def self.[](stage_name) - const_get("::Gitlab::CycleAnalytics::#{stage_name.to_s.camelize}Event") + CycleAnalytics.const_get("#{stage_name.to_s.camelize}Event") end end end diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb deleted file mode 100644 index 76e8decf36e..00000000000 --- a/lib/gitlab/cycle_analytics/issue_event.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Gitlab - module CycleAnalytics - class IssueEvent < BaseEvent - include IssueAllowed - - def initialize(*args) - @start_time_attrs = issue_table[:created_at] - @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at], - issue_metrics_table[:first_added_to_board_at]] - @projections = [issue_table[:title], - issue_table[:iid], - issue_table[:id], - issue_table[:created_at], - issue_table[:author_id]] - - super(*args) - end - - private - - def serialize(event) - AnalyticsIssueSerializer.new(project: @project).represent(event).as_json - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb new file mode 100644 index 00000000000..0d8da99455e --- /dev/null +++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb @@ -0,0 +1,23 @@ +module Gitlab + module CycleAnalytics + class IssueEventFetcher < BaseEventFetcher + include IssueAllowed + + def initialize(*args) + @projections = [issue_table[:title], + issue_table[:iid], + issue_table[:id], + issue_table[:created_at], + issue_table[:author_id]] + + super(*args) + end + + private + + def serialize(event) + AnalyticsIssueSerializer.new(project: @project).represent(event).as_json + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index c317872fb1d..14e72c7ea48 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -1,15 +1,20 @@ module Gitlab module CycleAnalytics class IssueStage < BaseStage - def description - "Time before an issue gets scheduled" + def initialize(*args) + @start_time_attrs = issue_table[:created_at] + @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at], + issue_metrics_table[:first_added_to_board_at]] + + super(*args) end - def median - @fetcher.median(:issue, - Issue.arel_table[:created_at], - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]]) + def stage + :issue + end + + def description + "Time before an issue gets scheduled" end end end diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb index 0542fbfb38d..865abd0fa6c 100644 --- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb +++ b/lib/gitlab/cycle_analytics/metrics_fetcher.rb @@ -9,14 +9,15 @@ module Gitlab DEPLOYMENT_METRIC_STAGES = %i[production staging] - def initialize(project:, from:, branch:) + def initialize(project:, from:, branch:, stage:) @project = project @from = from @branch = branch + @stage = stage end - def median(name, start_time_attrs, end_time_attrs) - cte_table = Arel::Table.new("cte_table_for_#{name}") + def median + cte_table = Arel::Table.new("cte_table_for_#{@stage.stage}") # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). @@ -24,24 +25,26 @@ module Gitlab # cycle analytics stage. interval_query = Arel::Nodes::As.new( cte_table, - subtract_datetimes(base_query_for(name), start_time_attrs, end_time_attrs, name.to_s)) + subtract_datetimes(base_query_for(name), @stage.start_time_attrs, @stage.end_time_attrs, @stage.stage.to_s)) median_datetime(cte_table, interval_query, name) end - def events(stage_class) - ActiveRecord::Base.connection.exec_query(events_query(stage_class).to_sql) + def events + ActiveRecord::Base.connection.exec_query(events_query.to_sql) end private - def events_query(stage_class) - base_query = base_query_for(stage_class.stage) - diff_fn = subtract_datetimes_diff(base_query, stage_class.start_time_attrs, stage_class.end_time_attrs) + def events_query + base_query = base_query_for(@stage.stage) + event = @stage.event - stage_class.custom_query(base_query) + diff_fn = subtract_datetimes_diff(base_query, @stage.start_time_attrs, @stage.end_time_attrs) - base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *stage_class.projections).order(stage_class.order.desc) + event_instance.custom_query(base_query) + + base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *event.projections).order(event.order.desc) end # Join table with a row for every pair (where the merge request diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event.rb deleted file mode 100644 index 4b06143495b..00000000000 --- a/lib/gitlab/cycle_analytics/plan_event.rb +++ /dev/null @@ -1,45 +0,0 @@ -module Gitlab - module CycleAnalytics - class PlanEvent < BaseEvent - def initialize(*args) - @start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at] - @end_time_attrs = [issue_metrics_table[:first_added_to_board_at], - issue_metrics_table[:first_mentioned_in_commit_at]] - @projections = [mr_diff_table[:st_commits].as('commits'), - issue_metrics_table[:first_mentioned_in_commit_at]] - - super(*args) - end - - def custom_query(base_query) - base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id])) - end - - private - - def serialize(event) - st_commit = first_time_reference_commit(event.delete('commits'), event) - - return unless st_commit - - serialize_commit(event, st_commit, query) - end - - def first_time_reference_commit(commits, event) - return nil if commits.blank? - - YAML.load(commits).find do |commit| - next unless commit[:committed_date] && event['first_mentioned_in_commit_at'] - - commit[:committed_date].to_i == DateTime.parse(event['first_mentioned_in_commit_at'].to_s).to_i - end - end - - def serialize_commit(event, st_commit, query) - commit = Commit.new(Gitlab::Git::Commit.new(st_commit), @project) - - AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit).as_json - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb new file mode 100644 index 00000000000..3e23c5644d3 --- /dev/null +++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb @@ -0,0 +1,42 @@ +module Gitlab + module CycleAnalytics + class PlanEventFetcher < BaseEventFetcher + def initialize(*args) + @projections = [mr_diff_table[:st_commits].as('commits'), + issue_metrics_table[:first_mentioned_in_commit_at]] + + super(*args) + end + + def custom_query(base_query) + base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id])) + end + + private + + def serialize(event) + st_commit = first_time_reference_commit(event.delete('commits'), event) + + return unless st_commit + + serialize_commit(event, st_commit, query) + end + + def first_time_reference_commit(commits, event) + return nil if commits.blank? + + YAML.load(commits).find do |commit| + next unless commit[:committed_date] && event['first_mentioned_in_commit_at'] + + commit[:committed_date].to_i == DateTime.parse(event['first_mentioned_in_commit_at'].to_s).to_i + end + end + + def serialize_commit(event, st_commit, query) + commit = Commit.new(Gitlab::Git::Commit.new(st_commit), @project) + + AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit).as_json + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index 5e6dd30d9e3..de2d5aaeb23 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -1,15 +1,20 @@ module Gitlab module CycleAnalytics class PlanStage < BaseStage - def description - "Time before an issue starts implementation" + def initialize(*args) + @start_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at], + issue_metrics_table[:first_added_to_board_at]] + @end_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] + + super(*args) end - def median - @fetcher.median(:plan, - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]], - Issue::Metrics.arel_table[:first_mentioned_in_commit_at]) + def stage + :code + end + + def description + "Time before an issue starts implementation" end end end diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/production_event.rb deleted file mode 100644 index c03cd4f4909..00000000000 --- a/lib/gitlab/cycle_analytics/production_event.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Gitlab - module CycleAnalytics - class ProductionEvent < BaseEvent - include IssueAllowed - - def initialize(*args) - @start_time_attrs = issue_table[:created_at] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] - @projections = [issue_table[:title], - issue_table[:iid], - issue_table[:id], - issue_table[:created_at], - issue_table[:author_id]] - - super(*args) - end - - private - - def serialize(event) - AnalyticsIssueSerializer.new(project: @project).represent(event).as_json - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb new file mode 100644 index 00000000000..b7eff7d22f4 --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb @@ -0,0 +1,23 @@ +module Gitlab + module CycleAnalytics + class ProductionEventFetcher < BaseEventFetcher + include IssueAllowed + + def initialize(*args) + @projections = [issue_table[:title], + issue_table[:iid], + issue_table[:id], + issue_table[:created_at], + issue_table[:author_id]] + + super(*args) + end + + private + + def serialize(event) + AnalyticsIssueSerializer.new(project: @project).represent(event).as_json + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb index acd2f7b2b3b..104c6d3fd30 100644 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -1,14 +1,19 @@ module Gitlab module CycleAnalytics class ProductionStage < BaseStage - def description - "From issue creation until deploy to production" + def initialize(*args) + @start_time_attrs = issue_table[:created_at] + @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] + + super(*args) end - def median - @fetcher.median(:production, - Issue.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + def stage + :production + end + + def description + "From issue creation until deploy to production" end end end diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/review_event.rb deleted file mode 100644 index 3f9ffa9657b..00000000000 --- a/lib/gitlab/cycle_analytics/review_event.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Gitlab - module CycleAnalytics - class ReviewEvent < BaseEvent - include MergeRequestAllowed - - def initialize(*args) - @start_time_attrs = mr_table[:created_at] - @end_time_attrs = mr_metrics_table[:merged_at] - @projections = [mr_table[:title], - mr_table[:iid], - mr_table[:id], - mr_table[:created_at], - mr_table[:state], - mr_table[:author_id]] - - super(*args) - end - - def serialize(event) - AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb new file mode 100644 index 00000000000..4df0bd06393 --- /dev/null +++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb @@ -0,0 +1,22 @@ +module Gitlab + module CycleAnalytics + class ReviewEventFetcher < BaseEventFetcher + include MergeRequestAllowed + + def initialize(*args) + @projections = [mr_table[:title], + mr_table[:iid], + mr_table[:id], + mr_table[:created_at], + mr_table[:state], + mr_table[:author_id]] + + super(*args) + end + + def serialize(event) + AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index c7b5e34e16a..c7bbd29693b 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -1,14 +1,19 @@ module Gitlab module CycleAnalytics class ReviewStage < BaseStage - def description - "Time between merge request creation and merge/close" + def initialize(*args) + @start_time_attrs = mr_table[:created_at] + @end_time_attrs = mr_metrics_table[:merged_at] + + super(*args) end - def median - @fetcher.median(:review, - MergeRequest.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:merged_at]) + def stage + :review + end + + def description + "Time between merge request creation and merge/close" end end end diff --git a/lib/gitlab/cycle_analytics/stage.rb b/lib/gitlab/cycle_analytics/stage.rb index acf746db6cd..28e0455df59 100644 --- a/lib/gitlab/cycle_analytics/stage.rb +++ b/lib/gitlab/cycle_analytics/stage.rb @@ -2,7 +2,7 @@ module Gitlab module CycleAnalytics module Stage def self.[](stage_name) - const_get("::Gitlab::CycleAnalytics::#{stage_name.to_s.camelize}Stage") + CycleAnalytics.const_get("#{stage_name.to_s.camelize}Stage") end end end diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event.rb deleted file mode 100644 index eae18b447f0..00000000000 --- a/lib/gitlab/cycle_analytics/staging_event.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Gitlab - module CycleAnalytics - class StagingEvent < BaseEvent - def initialize(*args) - @start_time_attrs = mr_metrics_table[:merged_at] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] - @projections = [build_table[:id]] - @order = build_table[:created_at] - - super(*args) - end - - def fetch - Updater.update!(event_result, from: 'id', to: 'build', klass: ::Ci::Build) - - super - end - - def custom_query(base_query) - base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) - end - - private - - def serialize(event) - AnalyticsBuildSerializer.new.represent(event['build']).as_json - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb new file mode 100644 index 00000000000..ea98e211ad6 --- /dev/null +++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb @@ -0,0 +1,28 @@ +module Gitlab + module CycleAnalytics + class StagingEventFetcher < BaseEventFetcher + def initialize(*args) + @projections = [build_table[:id]] + @order = build_table[:created_at] + + super(*args) + end + + def fetch + Updater.update!(event_result, from: 'id', to: 'build', klass: ::Ci::Build) + + super + end + + def custom_query(base_query) + base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) + end + + private + + def serialize(event) + AnalyticsBuildSerializer.new.represent(event['build']).as_json + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index b715a9453c7..079b26760bb 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -1,14 +1,19 @@ module Gitlab module CycleAnalytics class StagingStage < BaseStage - def description - "From merge request merge until deploy to production" + def initialize(*args) + @start_time_attrs = mr_metrics_table[:merged_at] + @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] + + super(*args) end - def median - @fetcher.median(:staging, - MergeRequest::Metrics.arel_table[:merged_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + def stage + :staging + end + + def description + "From merge request merge until deploy to production" end end end diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb deleted file mode 100644 index d0736672adf..00000000000 --- a/lib/gitlab/cycle_analytics/test_event.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Gitlab - module CycleAnalytics - class TestEvent < StagingEvent - def initialize(*args) - super(*args) - - @start_time_attrs = mr_metrics_table[:latest_build_started_at] - @end_time_attrs = mr_metrics_table[:latest_build_finished_at] - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/test_event_fetcher.rb b/lib/gitlab/cycle_analytics/test_event_fetcher.rb new file mode 100644 index 00000000000..a2589c6601a --- /dev/null +++ b/lib/gitlab/cycle_analytics/test_event_fetcher.rb @@ -0,0 +1,6 @@ +module Gitlab + module CycleAnalytics + class TestEventFetcher < StagingEventFetcher + end + end +end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index 58f72bb405e..a105e5f2b1f 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -1,14 +1,19 @@ module Gitlab module CycleAnalytics class TestStage < BaseStage - def description - "Total test time for all commits/merges" + def initialize(*args) + @start_time_attrs = mr_metrics_table[:latest_build_started_at] + @end_time_attrs = mr_metrics_table[:latest_build_finished_at] + + super(*args) end - def median - @fetcher.median(:test, - MergeRequest::Metrics.arel_table[:latest_build_started_at], - MergeRequest::Metrics.arel_table[:latest_build_finished_at]) + def stage + :test + end + + def description + "Total test time for all commits/merges" end end end diff --git a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb new file mode 100644 index 00000000000..abfd60d7f6a --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::CodeEventFetcher do + let(:stage_name) { :code } + + it_behaves_like 'default query config' do + it 'does not have the default order' do + expect(event.order).not_to eq(event.start_time_attrs) + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb deleted file mode 100644 index 0673906e678..00000000000 --- a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::CodeEvent do - let(:stage_name) { :code } - - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb new file mode 100644 index 00000000000..f4d995d072f --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::IssueEventFetcher do + let(:stage_name) { :issue } + + it_behaves_like 'default query config' do + it 'has the default order' do + expect(event.order).to eq(event.start_time_attrs) + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb deleted file mode 100644 index 7967d3727db..00000000000 --- a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::IssueEvent do - let(:stage_name) { :issue } - - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb new file mode 100644 index 00000000000..679779de51e --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::PlanEventFetcher do + let(:stage_name) { :plan } + + it_behaves_like 'default query config' do + it 'has the default order' do + expect(event.order).to eq(event.start_time_attrs) + end + + context 'no commits' do + it 'does not blow up if there are no commits' do + allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:events).and_return([{}]) + + expect { event.fetch }.not_to raise_error + end + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb deleted file mode 100644 index df407e51c64..00000000000 --- a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::PlanEvent do - let(:stage_name) { :plan } - - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - - context 'no commits' do - it 'does not blow up if there are no commits' do - allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:events).and_return([{}]) - - expect { event.fetch }.not_to raise_error - end - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb new file mode 100644 index 00000000000..a9126b8fa1c --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::ProductionEventFetcher do + let(:stage_name) { :production } + + it_behaves_like 'default query config' do + it 'has the default order' do + expect(event.order).to eq(event.start_time_attrs) + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb deleted file mode 100644 index 99ed9a0ab5c..00000000000 --- a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::ProductionEvent do - let(:stage_name) { :production } - - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb new file mode 100644 index 00000000000..c3e66dcb861 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::ReviewEventFetcher do + let(:stage_name) { :review } + + it_behaves_like 'default query config' do + it 'has the default order' do + expect(event.order).to eq(event.start_time_attrs) + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb deleted file mode 100644 index efc40d4ca4a..00000000000 --- a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::ReviewEvent do - let(:stage_name) { :review } - - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb index 5e1c7531fb5..60ec87255c8 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -4,10 +4,11 @@ shared_examples 'default query config' do let(:fetcher) do Gitlab::CycleAnalytics::MetricsFetcher.new(project: create(:empty_project), from: 1.day.ago, - branch: nil) + branch: nil, + stage: stage_name) end - let(:event) { described_class.new(fetcher: fetcher, options: {}) } + let(:event) { described_class.new(fetcher: fetcher, options: {}, stage: stage_name) } it 'has the start attributes' do expect(event.start_time_attrs).not_to be_nil diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb index cfb5dc12ff1..8cc7875258e 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -5,7 +5,7 @@ shared_examples 'base stage' do before do allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:median).and_return(1.12) - allow_any_instance_of(Gitlab::CycleAnalytics::BaseEvent).to receive(:event_result).and_return({}) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) end it 'has the median data value' do diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb new file mode 100644 index 00000000000..8338e17b96d --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::StagingEventFetcher do + let(:stage_name) { :staging } + + it_behaves_like 'default query config' do + it 'does not have the default order' do + expect(event.order).not_to eq(event.start_time_attrs) + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb deleted file mode 100644 index b7ab477067c..00000000000 --- a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::StagingEvent do - let(:stage_name) { :staging } - - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb new file mode 100644 index 00000000000..9d4f7667f1d --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::TestEventFetcher do + let(:stage_name) { :test } + + it_behaves_like 'default query config' do + it 'does not have the default order' do + expect(event.order).not_to eq(event.start_time_attrs) + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb deleted file mode 100644 index a4fc8963e5b..00000000000 --- a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::TestEvent do - let(:stage_name) { :test } - - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - end - end -end diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb index 3627a21230f..5597fbed151 100644 --- a/spec/serializers/analytics_stage_serializer_spec.rb +++ b/spec/serializers/analytics_stage_serializer_spec.rb @@ -11,7 +11,7 @@ describe AnalyticsStageSerializer do before do allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:median).and_return(1.12) - allow_any_instance_of(Gitlab::CycleAnalytics::BaseEvent).to receive(:event_result).and_return({}) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) end it 'it generates payload for single object' do -- cgit v1.2.1 From 3f681f4cefb5eda594acaab2eaf1be18ebd9066c Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 5 Dec 2016 09:47:10 +0100 Subject: fix specs, refactor missing bits from events stuff --- lib/gitlab/cycle_analytics/base_event_fetcher.rb | 8 ++------ lib/gitlab/cycle_analytics/base_stage.rb | 4 +++- lib/gitlab/cycle_analytics/event.rb | 2 +- lib/gitlab/cycle_analytics/metrics_fetcher.rb | 9 ++++++--- spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb | 4 ++-- spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb | 6 +----- spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb | 4 ---- spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb | 6 +----- spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb | 6 +----- spec/lib/gitlab/cycle_analytics/shared_event_spec.rb | 8 -------- spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb | 8 ++++++++ spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb | 4 ++-- spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb | 4 ++-- 13 files changed, 29 insertions(+), 44 deletions(-) diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb index 0d851f81b1d..d4b2d665e59 100644 --- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb @@ -3,7 +3,7 @@ module Gitlab class BaseEventFetcher include MetricsTables - attr_reader :projections, :query, :stage + attr_reader :projections, :query, :stage, :order def initialize(fetcher:, options:, stage:) @fetcher = fetcher @@ -22,10 +22,6 @@ module Gitlab def custom_query(_base_query); end - def order - @order || @start_time_attrs - end - private def update_author! @@ -35,7 +31,7 @@ module Gitlab end def event_result - @event_result ||= @fetcher.events(self).to_a + @event_result ||= @fetcher.events.to_a end def serialize(_event) diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index f3b8bb6e1d3..f81a41bccb6 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -1,6 +1,8 @@ module Gitlab module CycleAnalytics class BaseStage + include MetricsTables + attr_accessor :start_time_attrs, :end_time_attrs def initialize(project:, options:) @@ -13,7 +15,7 @@ module Gitlab end def event - @event ||= Gitlab::CycleAnalytics::Event[stage].new(fetcher: @fetcher, options: @options) + @event ||= Gitlab::CycleAnalytics::Event[stage].new(fetcher: @fetcher, options: @options, stage: stage) end def events diff --git a/lib/gitlab/cycle_analytics/event.rb b/lib/gitlab/cycle_analytics/event.rb index bb3a5722a0f..1ba7bc08ee5 100644 --- a/lib/gitlab/cycle_analytics/event.rb +++ b/lib/gitlab/cycle_analytics/event.rb @@ -2,7 +2,7 @@ module Gitlab module CycleAnalytics module Event def self.[](stage_name) - CycleAnalytics.const_get("#{stage_name.to_s.camelize}Event") + CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher") end end end diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb index 865abd0fa6c..dd291840ecd 100644 --- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb +++ b/lib/gitlab/cycle_analytics/metrics_fetcher.rb @@ -38,13 +38,16 @@ module Gitlab def events_query base_query = base_query_for(@stage.stage) - event = @stage.event diff_fn = subtract_datetimes_diff(base_query, @stage.start_time_attrs, @stage.end_time_attrs) - event_instance.custom_query(base_query) + @stage.event.custom_query(base_query) - base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *event.projections).order(event.order.desc) + base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *@stage.event.projections).order(order.desc) + end + + def order + @stage.event.order || @stage.start_time_attrs.is_a?(Array) ? @stage.start_time_attrs.first : @stage.start_time_attrs end # Join table with a row for every pair (where the merge request diff --git a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb index abfd60d7f6a..0267e8c2f69 100644 --- a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb @@ -5,8 +5,8 @@ describe Gitlab::CycleAnalytics::CodeEventFetcher do let(:stage_name) { :code } it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) + it 'has a default order' do + expect(event.order).not_to be_nil end end end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb index f4d995d072f..fd9fa2fee49 100644 --- a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb @@ -4,9 +4,5 @@ require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::IssueEventFetcher do let(:stage_name) { :issue } - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end + it_behaves_like 'default query config' end diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb index 679779de51e..1c3c1728fc6 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb @@ -5,10 +5,6 @@ describe Gitlab::CycleAnalytics::PlanEventFetcher do let(:stage_name) { :plan } it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - context 'no commits' do it 'does not blow up if there are no commits' do allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:events).and_return([{}]) diff --git a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb index a9126b8fa1c..74001181305 100644 --- a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb @@ -4,9 +4,5 @@ require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::ProductionEventFetcher do let(:stage_name) { :production } - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end + it_behaves_like 'default query config' end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb index c3e66dcb861..4f67c95ed4c 100644 --- a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb @@ -4,9 +4,5 @@ require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::ReviewEventFetcher do let(:stage_name) { :review } - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end + it_behaves_like 'default query config' end diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb index 60ec87255c8..725f9a558f5 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -10,18 +10,10 @@ shared_examples 'default query config' do let(:event) { described_class.new(fetcher: fetcher, options: {}, stage: stage_name) } - it 'has the start attributes' do - expect(event.start_time_attrs).not_to be_nil - end - it 'has the stage attribute' do expect(event.stage).not_to be_nil end - it 'has the end attributes' do - expect(event.end_time_attrs).not_to be_nil - end - it 'has the projection attributes' do expect(event.projections).not_to be_nil end diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb index 8cc7875258e..c88e3e22f5c 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -8,6 +8,14 @@ shared_examples 'base stage' do allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) end + it 'has the start attributes' do + expect(stage.start_time_attrs).not_to be_nil + end + + it 'has the end attributes' do + expect(stage.end_time_attrs).not_to be_nil + end + it 'has the median data value' do expect(stage.median_data[:value]).not_to be_nil end diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb index 8338e17b96d..bbc82496340 100644 --- a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb @@ -5,8 +5,8 @@ describe Gitlab::CycleAnalytics::StagingEventFetcher do let(:stage_name) { :staging } it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) + it 'has a default order' do + expect(event.order).not_to be_nil end end end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb index 9d4f7667f1d..6639fa54e0e 100644 --- a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb @@ -5,8 +5,8 @@ describe Gitlab::CycleAnalytics::TestEventFetcher do let(:stage_name) { :test } it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) + it 'has a default order' do + expect(event.order).not_to be_nil end end end -- cgit v1.2.1 From 099aa124ebefac0ab490ab8e28294e0ea78279de Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 5 Dec 2016 10:45:07 +0100 Subject: fix plan stage issue and some spec failures --- lib/gitlab/cycle_analytics/base_stage.rb | 4 +++- lib/gitlab/cycle_analytics/metrics_fetcher.rb | 6 +++++- lib/gitlab/cycle_analytics/plan_stage.rb | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index f81a41bccb6..c2605364ff0 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -15,7 +15,9 @@ module Gitlab end def event - @event ||= Gitlab::CycleAnalytics::Event[stage].new(fetcher: @fetcher, options: @options, stage: stage) + @event ||= Gitlab::CycleAnalytics::Event[stage].new(fetcher: @fetcher, + options: @options, + stage: stage) end def events diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb index dd291840ecd..559dbc0e8fc 100644 --- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb +++ b/lib/gitlab/cycle_analytics/metrics_fetcher.rb @@ -47,7 +47,11 @@ module Gitlab end def order - @stage.event.order || @stage.start_time_attrs.is_a?(Array) ? @stage.start_time_attrs.first : @stage.start_time_attrs + @stage.event.order || default_order + end + + def default_order + @stage.start_time_attrs.is_a?(Array) ? @stage.start_time_attrs.first : @stage.start_time_attrs end # Join table with a row for every pair (where the merge request diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index de2d5aaeb23..f8c9b9c4495 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -10,7 +10,7 @@ module Gitlab end def stage - :code + :plan end def description -- cgit v1.2.1 From 056b0f199388ffd4d3156c59a344f284131f7cc6 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 5 Dec 2016 12:07:31 +0100 Subject: fix missing refactor in metrics fetcher --- lib/gitlab/cycle_analytics/metrics_fetcher.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb index 559dbc0e8fc..4115c092c0d 100644 --- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb +++ b/lib/gitlab/cycle_analytics/metrics_fetcher.rb @@ -25,9 +25,9 @@ module Gitlab # cycle analytics stage. interval_query = Arel::Nodes::As.new( cte_table, - subtract_datetimes(base_query_for(name), @stage.start_time_attrs, @stage.end_time_attrs, @stage.stage.to_s)) + subtract_datetimes(base_query_for(@stage.stage), @stage.start_time_attrs, @stage.end_time_attrs, @stage.stage.to_s)) - median_datetime(cte_table, interval_query, name) + median_datetime(cte_table, interval_query, @stage.stage) end def events -- cgit v1.2.1 From bbb9f840824aec2467ed2580cae4d9b42b85a7d4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 5 Dec 2016 13:28:53 +0100 Subject: a few more fixes --- app/models/cycle_analytics.rb | 2 +- lib/gitlab/cycle_analytics/issue_event_fetcher.rb | 17 ----------------- lib/gitlab/cycle_analytics/production_event_fetcher.rb | 2 +- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index 78aebd828d7..37ac56310ef 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -32,7 +32,7 @@ class CycleAnalytics def stats_per_stage STAGES.map do |stage_name| - self[stage_name].new(project: @project, options: @options).median_data + self[stage_name].median_data end end end diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb index 0d8da99455e..69ba7c3cc9c 100644 --- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb @@ -1,23 +1,6 @@ module Gitlab module CycleAnalytics class IssueEventFetcher < BaseEventFetcher - include IssueAllowed - - def initialize(*args) - @projections = [issue_table[:title], - issue_table[:iid], - issue_table[:id], - issue_table[:created_at], - issue_table[:author_id]] - - super(*args) - end - - private - - def serialize(event) - AnalyticsIssueSerializer.new(project: @project).represent(event).as_json - end end end end diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb index b7eff7d22f4..882e780874f 100644 --- a/lib/gitlab/cycle_analytics/production_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb @@ -1,6 +1,6 @@ module Gitlab module CycleAnalytics - class ProductionEventFetcher < BaseEventFetcher + class ProductionEventFetcher < IssueEventFetcher include IssueAllowed def initialize(*args) -- cgit v1.2.1 From 834bcacbaec837d8ec0a269f111bca769843bcb4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 7 Dec 2016 09:25:11 +0100 Subject: fix refactor of production event fetcher --- lib/gitlab/cycle_analytics/issue_event_fetcher.rb | 17 +++++++++++++++++ lib/gitlab/cycle_analytics/production_event_fetcher.rb | 17 ----------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb index 69ba7c3cc9c..0d8da99455e 100644 --- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb @@ -1,6 +1,23 @@ module Gitlab module CycleAnalytics class IssueEventFetcher < BaseEventFetcher + include IssueAllowed + + def initialize(*args) + @projections = [issue_table[:title], + issue_table[:iid], + issue_table[:id], + issue_table[:created_at], + issue_table[:author_id]] + + super(*args) + end + + private + + def serialize(event) + AnalyticsIssueSerializer.new(project: @project).represent(event).as_json + end end end end diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb index 882e780874f..0fa2e87f673 100644 --- a/lib/gitlab/cycle_analytics/production_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb @@ -1,23 +1,6 @@ module Gitlab module CycleAnalytics class ProductionEventFetcher < IssueEventFetcher - include IssueAllowed - - def initialize(*args) - @projections = [issue_table[:title], - issue_table[:iid], - issue_table[:id], - issue_table[:created_at], - issue_table[:author_id]] - - super(*args) - end - - private - - def serialize(event) - AnalyticsIssueSerializer.new(project: @project).represent(event).as_json - end end end end -- cgit v1.2.1 From 982d5a050667c517bbc996a08ca0922f2c5fbfb4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 9 Dec 2016 12:41:15 +0100 Subject: refactored metrics fetcher - merged into stage and events --- lib/gitlab/cycle_analytics/base_event_fetcher.rb | 27 +++++-- lib/gitlab/cycle_analytics/base_query.rb | 31 ++++++++ lib/gitlab/cycle_analytics/base_stage.rb | 36 +++++---- lib/gitlab/cycle_analytics/code_stage.rb | 2 +- lib/gitlab/cycle_analytics/issue_stage.rb | 2 +- lib/gitlab/cycle_analytics/metrics_fetcher.rb | 86 ---------------------- lib/gitlab/cycle_analytics/plan_event_fetcher.rb | 4 +- lib/gitlab/cycle_analytics/plan_stage.rb | 2 +- lib/gitlab/cycle_analytics/production_helper.rb | 9 +++ lib/gitlab/cycle_analytics/production_stage.rb | 9 ++- lib/gitlab/cycle_analytics/review_stage.rb | 2 +- .../cycle_analytics/staging_event_fetcher.rb | 4 +- lib/gitlab/cycle_analytics/staging_stage.rb | 4 +- lib/gitlab/cycle_analytics/summary/commit.rb | 7 +- lib/gitlab/cycle_analytics/test_stage.rb | 10 ++- .../gitlab/cycle_analytics/shared_event_spec.rb | 5 +- 16 files changed, 119 insertions(+), 121 deletions(-) create mode 100644 lib/gitlab/cycle_analytics/base_query.rb delete mode 100644 lib/gitlab/cycle_analytics/metrics_fetcher.rb create mode 100644 lib/gitlab/cycle_analytics/production_helper.rb diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb index d4b2d665e59..8b4ccfd5363 100644 --- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb @@ -1,15 +1,14 @@ module Gitlab module CycleAnalytics class BaseEventFetcher - include MetricsTables + include BaseQuery attr_reader :projections, :query, :stage, :order - def initialize(fetcher:, options:, stage:) - @fetcher = fetcher - @project = fetcher.project - @options = options + def initialize(project:, stage:, options:) + @project = project @stage = stage + @options = options end def fetch @@ -20,8 +19,6 @@ module Gitlab end.compact end - def custom_query(_base_query); end - private def update_author! @@ -31,7 +28,21 @@ module Gitlab end def event_result - @event_result ||= @fetcher.events.to_a + @event_result ||= ActiveRecord::Base.connection.exec_query(events_query.to_sql).to_a + end + + def events_query + diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs]) + + base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc) + end + + def order + @order || default_order + end + + def default_order + @options[:start_time_attrs].is_a?(Array) ? @options[:start_time_attrs].first : @options[:start_time_attrs] end def serialize(_event) diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb new file mode 100644 index 00000000000..d560dca45c8 --- /dev/null +++ b/lib/gitlab/cycle_analytics/base_query.rb @@ -0,0 +1,31 @@ +module Gitlab + module CycleAnalytics + module BaseQuery + include MetricsTables + include Gitlab::Database::Median + include Gitlab::Database::DateTime + + private + + def base_query + @base_query ||= stage_query + end + + def stage_query + query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])). + join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])). + where(issue_table[:project_id].eq(@project.id)). + where(issue_table[:deleted_at].eq(nil)). + where(issue_table[:created_at].gteq(@options[:from])) + + # Load merge_requests + query = query.join(mr_table, Arel::Nodes::OuterJoin). + on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])). + join(mr_metrics_table). + on(mr_table[:id].eq(mr_metrics_table[:merge_request_id])) + + query + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index c2605364ff0..afec16d1818 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -1,23 +1,17 @@ module Gitlab module CycleAnalytics class BaseStage - include MetricsTables - - attr_accessor :start_time_attrs, :end_time_attrs + include BaseQuery def initialize(project:, options:) @project = project @options = options - @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, - from: options[:from], - branch: options[:branch], - stage: self) end def event - @event ||= Gitlab::CycleAnalytics::Event[stage].new(fetcher: @fetcher, - options: @options, - stage: stage) + @event ||= Gitlab::CycleAnalytics::Event[name].new(project: @project, + stage: name, + options: event_options) end def events @@ -29,17 +23,31 @@ module Gitlab end def title - stage.to_s.capitalize + name.to_s.capitalize end def median - @fetcher.median + cte_table = Arel::Table.new("cte_table_for_#{name}") + + # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). + # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). + # We compute the (end_time - start_time) interval, and give it an alias based on the current + # cycle analytics stage. + interval_query = Arel::Nodes::As.new( + cte_table, + subtract_datetimes(base_query, @start_time_attrs, @end_time_attrs, name.to_s)) + + median_datetime(cte_table, interval_query, name) + end + + def name + raise NotImplementedError.new("Expected #{self.name} to implement name") end private - def stage - class_name_for('Stage') + def event_options + @options.merge(start_time_attrs: @start_time_attrs, end_time_attrs: @end_time_attrs) end end end diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index 977d0d0210c..111c0e99633 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -8,7 +8,7 @@ module Gitlab super(*args) end - def stage + def name :code end diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index 14e72c7ea48..d320458d7fd 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -9,7 +9,7 @@ module Gitlab super(*args) end - def stage + def name :issue end diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb deleted file mode 100644 index 4115c092c0d..00000000000 --- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb +++ /dev/null @@ -1,86 +0,0 @@ -module Gitlab - module CycleAnalytics - class MetricsFetcher - include Gitlab::Database::Median - include Gitlab::Database::DateTime - include MetricsTables - - attr_reader :project - - DEPLOYMENT_METRIC_STAGES = %i[production staging] - - def initialize(project:, from:, branch:, stage:) - @project = project - @from = from - @branch = branch - @stage = stage - end - - def median - cte_table = Arel::Table.new("cte_table_for_#{@stage.stage}") - - # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). - # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). - # We compute the (end_time - start_time) interval, and give it an alias based on the current - # cycle analytics stage. - interval_query = Arel::Nodes::As.new( - cte_table, - subtract_datetimes(base_query_for(@stage.stage), @stage.start_time_attrs, @stage.end_time_attrs, @stage.stage.to_s)) - - median_datetime(cte_table, interval_query, @stage.stage) - end - - def events - ActiveRecord::Base.connection.exec_query(events_query.to_sql) - end - - private - - def events_query - base_query = base_query_for(@stage.stage) - - diff_fn = subtract_datetimes_diff(base_query, @stage.start_time_attrs, @stage.end_time_attrs) - - @stage.event.custom_query(base_query) - - base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *@stage.event.projections).order(order.desc) - end - - def order - @stage.event.order || default_order - end - - def default_order - @stage.start_time_attrs.is_a?(Array) ? @stage.start_time_attrs.first : @stage.start_time_attrs - end - - # Join table with a row for every pair (where the merge request - # closes the given issue) with issue and merge request metrics included. The metrics - # are loaded with an inner join, so issues / merge requests without metrics are - # automatically excluded. - def base_query_for(name) - # Load issues - query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])). - join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])). - where(issue_table[:project_id].eq(@project.id)). - where(issue_table[:deleted_at].eq(nil)). - where(issue_table[:created_at].gteq(@from)) - - query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch - - # Load merge_requests - query = query.join(mr_table, Arel::Nodes::OuterJoin). - on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])). - join(mr_metrics_table). - on(mr_table[:id].eq(mr_metrics_table[:merge_request_id])) - - if DEPLOYMENT_METRIC_STAGES.include?(name) - # Limit to merge requests that have been deployed to production after `@from` - query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from)) - end - - query - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb index 3e23c5644d3..88a8710dbe6 100644 --- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb @@ -8,8 +8,10 @@ module Gitlab super(*args) end - def custom_query(base_query) + def events_query base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id])) + + super end private diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index f8c9b9c4495..a7164e5c5b7 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -9,7 +9,7 @@ module Gitlab super(*args) end - def stage + def name :plan end diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb new file mode 100644 index 00000000000..d693443bfa4 --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_helper.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module ProductionHelper + def stage_query + super.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@options[:from])) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb index 104c6d3fd30..eb221c68324 100644 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -1,6 +1,8 @@ module Gitlab module CycleAnalytics class ProductionStage < BaseStage + include ProductionHelper + def initialize(*args) @start_time_attrs = issue_table[:created_at] @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] @@ -8,13 +10,18 @@ module Gitlab super(*args) end - def stage + def name :production end def description "From issue creation until deploy to production" end + + def query + # Limit to merge requests that have been deployed to production after `@from` + query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from)) + end end end end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index c7bbd29693b..72ce1ed1e16 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -8,7 +8,7 @@ module Gitlab super(*args) end - def stage + def name :review end diff --git a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb index ea98e211ad6..a34731a5fcd 100644 --- a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb @@ -14,8 +14,10 @@ module Gitlab super end - def custom_query(base_query) + def events_query base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) + + super end private diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index 079b26760bb..398c1b5989a 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -1,6 +1,8 @@ module Gitlab module CycleAnalytics class StagingStage < BaseStage + include ProductionHelper + def initialize(*args) @start_time_attrs = mr_metrics_table[:merged_at] @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] @@ -8,7 +10,7 @@ module Gitlab super(*args) end - def stage + def name :staging end diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb index ec3c067c0be..61a50762164 100644 --- a/lib/gitlab/cycle_analytics/summary/commit.rb +++ b/lib/gitlab/cycle_analytics/summary/commit.rb @@ -23,8 +23,11 @@ module Gitlab cmd << "--after=#{@from.iso8601}" cmd << sha - raw_output = IO.popen(cmd) { |io| io.read } - raw_output.lines.count + output, status = Gitlab::Popen.popen(cmd) { |io| io.read } + + raise IOError, output unless status.zero? + + output.lines.count end def ref diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index a105e5f2b1f..7e59745ffef 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -8,13 +8,21 @@ module Gitlab super(*args) end - def stage + def name :test end def description "Total test time for all commits/merges" end + + def stage_query + if @options[:branch] + super.where(build_table[:ref].eq(@options[:branch])) + else + super + end + end end end end diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb index 725f9a558f5..03b013ffae8 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -8,10 +8,11 @@ shared_examples 'default query config' do stage: stage_name) end - let(:event) { described_class.new(fetcher: fetcher, options: {}, stage: stage_name) } + let(project) + let(:event) { described_class.new(project: project, stage: stage_name, options: {}) } it 'has the stage attribute' do - expect(event.stage).not_to be_nil + expect(event.name).not_to be_nil end it 'has the projection attributes' do -- cgit v1.2.1 From 30c6703f0afbf570ca3e3613a55afcbc7094c4eb Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 9 Dec 2016 15:23:09 +0100 Subject: fix specs --- lib/gitlab/cycle_analytics/base_event_fetcher.rb | 8 ++++---- lib/gitlab/cycle_analytics/summary/commit.rb | 2 +- spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb | 2 +- spec/lib/gitlab/cycle_analytics/shared_event_spec.rb | 13 +++---------- spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb | 10 +--------- 5 files changed, 10 insertions(+), 25 deletions(-) diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb index 8b4ccfd5363..8d10ddf15d7 100644 --- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb @@ -19,6 +19,10 @@ module Gitlab end.compact end + def order + @order || default_order + end + private def update_author! @@ -37,10 +41,6 @@ module Gitlab base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc) end - def order - @order || default_order - end - def default_order @options[:start_time_attrs].is_a?(Array) ? @options[:start_time_attrs].first : @options[:start_time_attrs] end diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb index 61a50762164..7b8faa4d854 100644 --- a/lib/gitlab/cycle_analytics/summary/commit.rb +++ b/lib/gitlab/cycle_analytics/summary/commit.rb @@ -23,7 +23,7 @@ module Gitlab cmd << "--after=#{@from.iso8601}" cmd << sha - output, status = Gitlab::Popen.popen(cmd) { |io| io.read } + output, status = Gitlab::Popen.popen(cmd) raise IOError, output unless status.zero? diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb index 1c3c1728fc6..2e5dc5b5547 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::CycleAnalytics::PlanEventFetcher do it_behaves_like 'default query config' do context 'no commits' do it 'does not blow up if there are no commits' do - allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:events).and_return([{}]) + allow(event).to receive(:event_result).and_return([{}]) expect { event.fetch }.not_to raise_error end diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb index 03b013ffae8..9c5e57342e9 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -1,18 +1,11 @@ require 'spec_helper' shared_examples 'default query config' do - let(:fetcher) do - Gitlab::CycleAnalytics::MetricsFetcher.new(project: create(:empty_project), - from: 1.day.ago, - branch: nil, - stage: stage_name) - end - - let(project) - let(:event) { described_class.new(project: project, stage: stage_name, options: {}) } + let(:project) { create(:empty_project) } + let(:event) { described_class.new(project: project, stage: stage_name, options: { from: 1.day.ago }) } it 'has the stage attribute' do - expect(event.name).not_to be_nil + expect(event.stage).not_to be_nil end it 'has the projection attributes' do diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb index c88e3e22f5c..6f3883d80f8 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -4,18 +4,10 @@ shared_examples 'base stage' do let(:stage) { described_class.new(project: double, options: {}) } before do - allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:median).and_return(1.12) + allow(stage).to receive(:median).and_return(1.12) allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) end - it 'has the start attributes' do - expect(stage.start_time_attrs).not_to be_nil - end - - it 'has the end attributes' do - expect(stage.end_time_attrs).not_to be_nil - end - it 'has the median data value' do expect(stage.median_data[:value]).not_to be_nil end -- cgit v1.2.1 From 1b220df56d935f6f9560bdb54f744f17ecfa035b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 9 Dec 2016 15:28:08 +0100 Subject: fix bug retrieving medians --- lib/gitlab/cycle_analytics/base_stage.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index afec16d1818..9143792e044 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -35,7 +35,7 @@ module Gitlab # cycle analytics stage. interval_query = Arel::Nodes::As.new( cte_table, - subtract_datetimes(base_query, @start_time_attrs, @end_time_attrs, name.to_s)) + subtract_datetimes(base_query.dup, @start_time_attrs, @end_time_attrs, name.to_s)) median_datetime(cte_table, interval_query, name) end -- cgit v1.2.1 From 34875ce6b760980c1539c3ab88af0429e04d90bc Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 9 Dec 2016 16:38:59 +0100 Subject: fix serializer --- spec/serializers/analytics_stage_serializer_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb index 5597fbed151..f9951826683 100644 --- a/spec/serializers/analytics_stage_serializer_spec.rb +++ b/spec/serializers/analytics_stage_serializer_spec.rb @@ -10,7 +10,7 @@ describe AnalyticsStageSerializer do let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) } before do - allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:median).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(1.12) allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) end -- cgit v1.2.1 From 150a448596dda076f0893facbd621429738aba92 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 Jan 2017 12:48:01 +0100 Subject: refactored a bunch of stuff based on feedback --- app/models/cycle_analytics.rb | 4 ++-- lib/gitlab/cycle_analytics/base_event_fetcher.rb | 2 +- lib/gitlab/cycle_analytics/base_stage.rb | 20 ++++++++++---------- lib/gitlab/cycle_analytics/code_stage.rb | 9 +++++---- lib/gitlab/cycle_analytics/event.rb | 9 --------- lib/gitlab/cycle_analytics/event_fetcher.rb | 9 +++++++++ lib/gitlab/cycle_analytics/issue_stage.rb | 11 ++++++----- lib/gitlab/cycle_analytics/plan_stage.rb | 11 ++++++----- lib/gitlab/cycle_analytics/production_stage.rb | 9 +++++---- lib/gitlab/cycle_analytics/review_stage.rb | 9 +++++---- lib/gitlab/cycle_analytics/staging_stage.rb | 10 +++++----- lib/gitlab/cycle_analytics/test_stage.rb | 9 +++++---- spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb | 6 +++--- 13 files changed, 62 insertions(+), 56 deletions(-) delete mode 100644 lib/gitlab/cycle_analytics/event.rb create mode 100644 lib/gitlab/cycle_analytics/event_fetcher.rb diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index 37ac56310ef..054a6070bb8 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -17,7 +17,7 @@ class CycleAnalytics end def no_stats? - stats.map { |hash| hash[:value] }.compact.empty? + stats.all? { hash[:value].nil? } end def permissions(user:) @@ -32,7 +32,7 @@ class CycleAnalytics def stats_per_stage STAGES.map do |stage_name| - self[stage_name].median_data + self[stage_name].as_json end end end diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb index 8d10ddf15d7..0d8791d396b 100644 --- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb @@ -42,7 +42,7 @@ module Gitlab end def default_order - @options[:start_time_attrs].is_a?(Array) ? @options[:start_time_attrs].first : @options[:start_time_attrs] + [@options[:start_time_attrs]].flatten.first end def serialize(_event) diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index 9143792e044..7ff15051558 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -8,17 +8,11 @@ module Gitlab @options = options end - def event - @event ||= Gitlab::CycleAnalytics::Event[name].new(project: @project, - stage: name, - options: event_options) - end - def events - event.fetch + event_fetcher.fetch end - def median_data + def as_json AnalyticsStageSerializer.new.represent(self).as_json end @@ -35,7 +29,7 @@ module Gitlab # cycle analytics stage. interval_query = Arel::Nodes::As.new( cte_table, - subtract_datetimes(base_query.dup, @start_time_attrs, @end_time_attrs, name.to_s)) + subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s)) median_datetime(cte_table, interval_query, name) end @@ -46,8 +40,14 @@ module Gitlab private + def event_fetcher + @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: @project, + stage: name, + options: event_options) + end + def event_options - @options.merge(start_time_attrs: @start_time_attrs, end_time_attrs: @end_time_attrs) + @options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs) end end end diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index 111c0e99633..d1bc2055ba8 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -1,11 +1,12 @@ module Gitlab module CycleAnalytics class CodeStage < BaseStage - def initialize(*args) - @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] - @end_time_attrs = mr_table[:created_at] + def start_time_attrs + @start_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at] + end - super(*args) + def end_time_attrs + @end_time_attrs ||= mr_table[:created_at] end def name diff --git a/lib/gitlab/cycle_analytics/event.rb b/lib/gitlab/cycle_analytics/event.rb deleted file mode 100644 index 1ba7bc08ee5..00000000000 --- a/lib/gitlab/cycle_analytics/event.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Gitlab - module CycleAnalytics - module Event - def self.[](stage_name) - CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher") - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/event_fetcher.rb b/lib/gitlab/cycle_analytics/event_fetcher.rb new file mode 100644 index 00000000000..50e126cf00b --- /dev/null +++ b/lib/gitlab/cycle_analytics/event_fetcher.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module EventFetcher + def self.[](stage_name) + CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher") + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index d320458d7fd..d2068fbc38f 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -1,12 +1,13 @@ module Gitlab module CycleAnalytics class IssueStage < BaseStage - def initialize(*args) - @start_time_attrs = issue_table[:created_at] - @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at], - issue_metrics_table[:first_added_to_board_at]] + def start_time_attrs + @start_time_attrs ||= issue_table[:created_at] + end - super(*args) + def end_time_attrs + @end_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at], + issue_metrics_table[:first_added_to_board_at]] end def name diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index a7164e5c5b7..3b4dfc6a30e 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -1,12 +1,13 @@ module Gitlab module CycleAnalytics class PlanStage < BaseStage - def initialize(*args) - @start_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at], - issue_metrics_table[:first_added_to_board_at]] - @end_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] + def start_time_attrs + @start_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at], + issue_metrics_table[:first_added_to_board_at]] + end - super(*args) + def end_time_attrs + @end_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at] end def name diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb index eb221c68324..2a6bcc80116 100644 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -3,11 +3,12 @@ module Gitlab class ProductionStage < BaseStage include ProductionHelper - def initialize(*args) - @start_time_attrs = issue_table[:created_at] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] + def start_time_attrs + @start_time_attrs ||= issue_table[:created_at] + end - super(*args) + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at] end def name diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index 72ce1ed1e16..fbaa3010d81 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -1,11 +1,12 @@ module Gitlab module CycleAnalytics class ReviewStage < BaseStage - def initialize(*args) - @start_time_attrs = mr_table[:created_at] - @end_time_attrs = mr_metrics_table[:merged_at] + def start_time_attrs + @start_time_attrs ||= mr_table[:created_at] + end - super(*args) + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:merged_at] end def name diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index 398c1b5989a..945909a4d62 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -2,12 +2,12 @@ module Gitlab module CycleAnalytics class StagingStage < BaseStage include ProductionHelper + def start_time_attrs + @start_time_attrs ||= mr_metrics_table[:merged_at] + end - def initialize(*args) - @start_time_attrs = mr_metrics_table[:merged_at] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] - - super(*args) + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at] end def name diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index 7e59745ffef..0079d56e0e4 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -1,11 +1,12 @@ module Gitlab module CycleAnalytics class TestStage < BaseStage - def initialize(*args) - @start_time_attrs = mr_metrics_table[:latest_build_started_at] - @end_time_attrs = mr_metrics_table[:latest_build_finished_at] + def start_time_attrs + @start_time_attrs ||= mr_metrics_table[:latest_build_started_at] + end - super(*args) + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:latest_build_finished_at] end def name diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb index 6f3883d80f8..08425acbfc8 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -9,15 +9,15 @@ shared_examples 'base stage' do end it 'has the median data value' do - expect(stage.median_data[:value]).not_to be_nil + expect(stage.as_json[:value]).not_to be_nil end it 'has the median data stage' do - expect(stage.median_data[:title]).not_to be_nil + expect(stage.as_json[:title]).not_to be_nil end it 'has the median data description' do - expect(stage.median_data[:description]).not_to be_nil + expect(stage.as_json[:description]).not_to be_nil end it 'has the title' do -- cgit v1.2.1 From 1d775d9712d0c493ae171e37fe507f5160cd7d0e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 Jan 2017 14:32:30 +0100 Subject: fix spec --- app/models/cycle_analytics.rb | 2 +- lib/gitlab/cycle_analytics/base_stage.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index 054a6070bb8..d2e626c22e8 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -17,7 +17,7 @@ class CycleAnalytics end def no_stats? - stats.all? { hash[:value].nil? } + stats.all? { |hash| hash[:value].nil? } end def permissions(user:) diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index 7ff15051558..74bbcdcb3dd 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -42,8 +42,8 @@ module Gitlab def event_fetcher @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: @project, - stage: name, - options: event_options) + stage: name, + options: event_options) end def event_options -- cgit v1.2.1 From 476fce4e181825d6c3976563e1c163b01ffc3af0 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 17 Jan 2017 12:25:32 +0000 Subject: Disable all cops in .rubocop_todo.yml Cops with a max level are auto-generated with that set to the current maximum, even when they're supposed to be ignored. For now, the best option is to manually disable them. --- .rubocop_todo.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6d4d7170fe8..d581610162f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -62,7 +62,7 @@ Lint/UnusedMethodArgument: # Offense count: 93 # Configuration parameters: CountComments. Metrics/BlockLength: - Max: 288 + Enabled: false # Offense count: 3 # Cop supports --auto-correct. @@ -125,7 +125,7 @@ RSpec/MessageSpies: # Offense count: 3036 RSpec/MultipleExpectations: - Max: 37 + Enabled: false # Offense count: 2133 RSpec/NamedSubject: -- cgit v1.2.1 From 4fb4f61541a624608d6978208720d1db3970cfcd Mon Sep 17 00:00:00 2001 From: Regis Date: Tue, 17 Jan 2017 11:50:22 -0500 Subject: better UI fix - simple solution --- app/assets/javascripts/vue_pipelines_index/stage.js.es6 | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index f075a995846..34d75f01c9e 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -5,7 +5,6 @@ gl.VueStage = Vue.extend({ data() { return { - count: 0, builds: '', spinner: '', }; @@ -13,10 +12,8 @@ props: ['stage', 'svgs', 'match'], methods: { fetchBuilds() { - if (this.count > 0) return null; return this.$http.get(this.stage.dropdown_path) .then((response) => { - this.count += 1; this.builds = JSON.parse(response.body).html; }, () => { const flash = new Flash('Something went wrong on our end.'); @@ -55,13 +52,20 @@ :title='stage.title' data-placement="top" data-toggle="dropdown" - type="button"> + type="button" + > `, -- cgit v1.2.1 From 96b7865b4bc7b63c0d414946c31ec431bb892533 Mon Sep 17 00:00:00 2001 From: Regis Date: Tue, 17 Jan 2017 13:36:56 -0500 Subject: fix UI behaviour - only make new calls when button is clicked and dropdown is not displayed --- app/assets/javascripts/vue_pipelines_index/stage.js.es6 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 34d75f01c9e..7e14acb83d3 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -11,7 +11,13 @@ }, props: ['stage', 'svgs', 'match'], methods: { - fetchBuilds() { + fetchBuilds(e) { + const areaExpanded = e.currentTarget.attributes['aria-expanded']; + + console.log('HIT'); + + if (areaExpanded && (areaExpanded.textContent === 'true')) return null; + return this.$http.get(this.stage.dropdown_path) .then((response) => { this.builds = JSON.parse(response.body).html; @@ -47,7 +53,7 @@ template: `