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: diff --git a/Gemfile b/Gemfile index 07ff500dfea..83ba5d31b92 100644 --- a/Gemfile +++ b/Gemfile @@ -252,10 +252,9 @@ end group :development do gem 'foreman', '~> 0.78.0' - gem 'brakeman', '~> 3.3.0', require: false + gem 'brakeman', '~> 3.4.0', require: false gem 'letter_opener_web', '~> 1.3.0' - gem 'rerun', '~> 0.11.0' gem 'bullet', '~> 5.2.0', require: false gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false gem 'web-console', '~> 2.0' @@ -286,7 +285,7 @@ group :development, :test do gem 'minitest', '~> 5.7.0' # Generate Fake data - gem 'ffaker', '~> 2.0.0' + gem 'ffaker', '~> 2.4' gem 'capybara', '~> 2.6.2' gem 'capybara-screenshot', '~> 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index e2d7f94e571..104e6444803 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,7 +88,7 @@ GEM bootstrap-sass (3.3.6) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) - brakeman (3.3.2) + brakeman (3.4.1) browser (2.2.0) builder (3.2.2) bullet (5.2.0) @@ -198,7 +198,7 @@ GEM faraday_middleware-multi_json (0.0.6) faraday_middleware multi_json - ffaker (2.0.0) + ffaker (2.4.0) ffi (1.9.10) flay (2.6.1) ruby_parser (~> 3.0) @@ -407,9 +407,6 @@ GEM xml-simple licensee (8.0.0) rugged (>= 0.24b) - listen (3.0.5) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) little-plugger (1.1.4) logging (2.1.0) little-plugger (~> 1.1) @@ -580,9 +577,6 @@ GEM rainbow (2.1.0) raindrops (0.17.0) rake (10.5.0) - rb-fsevent (0.9.6) - rb-inotify (0.9.5) - ffi (>= 0.5.0) rblineprof (0.3.6) debugger-ruby_core_source (~> 1.3) rdoc (4.2.2) @@ -611,8 +605,6 @@ GEM redis-store (1.2.0) redis (>= 2.2) request_store (1.3.1) - rerun (0.11.0) - listen (~> 3.0) responders (2.3.0) railties (>= 4.2.0, < 5.1) rest-client (2.0.0) @@ -852,7 +844,7 @@ DEPENDENCIES better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) - brakeman (~> 3.3.0) + brakeman (~> 3.4.0) browser (~> 2.2) bullet (~> 5.2.0) bundler-audit (~> 0.5.0) @@ -877,7 +869,7 @@ DEPENDENCIES email_reply_trimmer (~> 0.1) email_spec (~> 1.6.0) factory_girl_rails (~> 4.7.0) - ffaker (~> 2.0.0) + ffaker (~> 2.4) flay (~> 2.6.1) fog-aws (~> 0.9) fog-core (~> 1.40) @@ -968,7 +960,6 @@ DEPENDENCIES redis-namespace (~> 1.5.2) redis-rails (~> 5.0.1) request_store (~> 1.3) - rerun (~> 0.11.0) responders (~> 2.0) rouge (~> 2.0) rqrcode-rails3 (~> 0.1.7) diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 5a7d823e84c..993f427c9fb 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */ /* global Turbolinks */ (function() { @@ -61,7 +61,5 @@ } return Admin; - })(); - }).call(this); diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index f60f27d1210..b4a8c827d7f 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */ (function() { var Api = { @@ -29,9 +29,9 @@ return $.ajax({ url: url, data: $.extend({ - search: query, - per_page: 20 - }, options), + search: query, + per_page: 20 + }, options), dataType: "json" }).done(function(groups) { return callback(groups); @@ -73,7 +73,7 @@ return $.ajax({ url: url, type: "POST", - data: {'label': data}, + data: { 'label': data }, dataType: "json" }).done(function(label) { return callback(label); diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js index 9417afc2ea7..8438de6cdf1 100644 --- a/app/assets/javascripts/aside.js +++ b/app/assets/javascripts/aside.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */ (function() { this.Aside = (function() { function Aside() { @@ -21,7 +21,5 @@ } return Aside; - })(); - }).call(this); diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index f45dbe4cbf2..b16a2c0f73a 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */ (function() { this.Autosave = (function() { function Autosave(field, key) { @@ -58,7 +58,5 @@ }; return Autosave; - })(); - }).call(this); diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 107a7978a87..629dc267337 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,9 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-plusplus, no-return-assign, camelcase, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, brace-style, no-underscore-dangle, no-return-assign, camelcase */ /* global Cookies */ (function() { this.AwardsHandler = (function() { - var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence + var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence function AwardsHandler() { this.aliases = gl.emojiAliases(); $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) { @@ -134,7 +134,7 @@ return this.decrementCounter($emojiButton, emoji); } else { counter = $emojiButton.find('.js-counter'); - counter.text(parseInt(counter.text()) + 1); + counter.text(parseInt(counter.text(), 10) + 1); $emojiButton.addClass('active'); this.addYouToUserList(votesBlock, emoji); return this.animateEmoji($emojiButton); @@ -211,10 +211,10 @@ }; AwardsHandler.prototype.toSentence = function(list) { - if(list.length <= 2){ + if (list.length <= 2) { return list.join(' and '); } - else{ + else { return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1]; } }; @@ -339,7 +339,7 @@ if (Cookies.get('frequently_used_emojis')) { frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); ul = $("
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.
+
/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/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index 423b095e69e..7a3628a39d7 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -79,7 +79,7 @@ Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the
initial translation of existing SVN revisions into the Git repository:
```
-subgit install $GIT_REPOS_PATH
+subgit install $GIT_REPO_PATH
```
After the initial translation is completed, the Git repository and the SVN
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..de12994c516
--- /dev/null
+++ b/doc/workflow/time_tracking.md
@@ -0,0 +1,73 @@
+# Time Tracking
+
+> Introduced in GitLab 8.14.
+
+Time Tracking allows you to track estimates and time spent on issues and merge
+requests within GitLab.
+
+## 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/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index b2bec369e0f..33a1c88e33c 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -35,7 +35,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
step 'I have group with projects' do
@group = create(:group)
- @project = create(:project, namespace: @group)
+ @project = create(:empty_project, namespace: @group)
@event = create(:closed_issue_event, project: @project)
@project.team << [current_user, :master]
@@ -54,8 +54,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
end
step 'group has a projects that does not belongs to me' do
- @forbidden_project1 = create(:project, group: @group)
- @forbidden_project2 = create(:project, group: @group)
+ @forbidden_project1 = create(:empty_project, group: @group)
+ @forbidden_project2 = create(:empty_project, group: @group)
end
step 'I should see 1 project at group list' do
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index 39c65bb6cde..4e15d79ae74 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -79,13 +79,13 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
def project
@project ||= begin
- project = create :project
+ project = create(:empty_project)
project.team << [current_user, :master]
project
end
end
def public_project
- @public_project ||= create :project, :public
+ @public_project ||= create(:empty_project, :public)
end
end
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 6777101fb15..909ffec3646 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -105,14 +105,14 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
def project
@project ||= begin
- project = create :project
+ project = create(:project, :repository)
project.team << [current_user, :master]
project
end
end
def public_project
- @public_project ||= create :project, :public
+ @public_project ||= create(:project, :public, :repository)
end
def forked_project
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index c1d1eca9116..70e23098dde 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -104,7 +104,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
group = owned_group
%w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
- project = create :project, path: path, group: group
+ project = create(:empty_project, path: path, group: group)
milestone = create :milestone, title: "Version 7.2", project: project
create(:label, project: project, title: 'bug')
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 0c88838767c..4dc87dc4d9c 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -109,7 +109,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'Group "Owned" has archived project' do
group = Group.find_by(name: 'Owned')
- @archived_project = create(:project, namespace: group, archived: true, path: "archived-project")
+ @archived_project = create(:empty_project, :archived, namespace: group, path: "archived-project")
end
step 'I should see "archived" label' do
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index ea480d2ad68..24cfbaad7fe 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -162,7 +162,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I have group with projects' do
@group = create(:group)
@group.add_owner(current_user)
- @project = create(:project, namespace: @group)
+ @project = create(:project, :repository, namespace: @group)
@event = create(:closed_issue_event, project: @project)
@project.team << [current_user, :master]
diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb
index 83b9ef48392..edf78f62f9a 100644
--- a/features/steps/project/deploy_keys.rb
+++ b/features/steps/project/deploy_keys.rb
@@ -46,11 +46,11 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
end
step 'other projects have deploy keys' do
- @second_project = create(:project, namespace: create(:group))
+ @second_project = create(:empty_project, namespace: create(:group))
@second_project.team << [current_user, :master]
create(:deploy_keys_project, project: @second_project)
- @third_project = create(:project, namespace: create(:group))
+ @third_project = create(:empty_project, namespace: create(:group))
@third_project.team << [current_user, :master]
create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first)
end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 70dbd030003..9a6c04fba7a 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -9,7 +9,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I am a member of project "Shop"' do
- @project = create(:project, name: "Shop")
+ @project = create(:project, :repository, name: "Shop")
@project.team << [@user, :reporter]
end
@@ -18,7 +18,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I already have a project named "Shop" in my namespace' do
- @my_project = create(:project, name: "Shop", namespace: current_user.namespace)
+ @my_project = create(:project, :repository, name: "Shop", namespace: current_user.namespace)
end
step 'I should see a "Name has already been taken" warning' do
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 6c14d835004..c0827ff8fc7 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -7,7 +7,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
step 'I am a member of project "Shop"' do
@project = Project.find_by(name: "Shop")
- @project ||= create(:project, name: "Shop")
+ @project ||= create(:project, :repository, name: "Shop")
@project.team << [@user, :reporter]
end
diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb
index 4fda0731e2f..0a3f4649870 100644
--- a/features/steps/project/merge_requests/acceptance.rb
+++ b/features/steps/project/merge_requests/acceptance.rb
@@ -28,7 +28,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
step 'There is an open Merge Request' do
@user = create(:user)
- @project = create(:project, :public)
+ @project = create(:project, :public, :repository)
@project_member = create(:project_member, :developer, user: @user, project: @project)
@merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
end
diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb
index efbc4831ce1..3cc4fe9dafb 100644
--- a/features/steps/project/merge_requests/revert.rb
+++ b/features/steps/project/merge_requests/revert.rb
@@ -35,7 +35,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
step 'There is an open Merge Request' do
@user = create(:user)
- @project = create(:project, :public)
+ @project = create(:project, :public, :repository)
@project_member = create(:project_member, :developer, user: @user, project: @project)
@merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
end
diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb
index 1ffd5cb9de5..92936f27c20 100644
--- a/features/steps/project/redirects.rb
+++ b/features/steps/project/redirects.rb
@@ -4,11 +4,11 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps
include SharedProject
step 'public project "Community"' do
- create :project, :public, name: 'Community'
+ create(:empty_project, :public, name: 'Community')
end
step 'private project "Enterprise"' do
- create :project, name: 'Enterprise'
+ create(:empty_project, :private, name: 'Enterprise')
end
step 'I visit project "Community" page' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 1cc9e37b075..f18adcadcce 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -6,7 +6,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
include RepoHelpers
step "I don't have write access" do
- @project = create(:project, name: "Other Project", path: "other-project")
+ @project = create(:project, :repository, name: "Other Project", path: "other-project")
@project.team << [@user, :reporter]
visit namespace_project_tree_path(@project.namespace, @project, root_ref)
end
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index dee6a8a5558..9183de76881 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -8,7 +8,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I own project "Delta"' do
@project = Project.find_by(name: "Delta")
- @project ||= create(:project, name: "Delta", namespace: @user.namespace)
+ @project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace)
@project.team << [@user, :master]
end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index c89f587f14d..6986c7ede56 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -137,7 +137,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
step 'I share project with group "OpenSource"' do
project = Project.find_by(name: 'Shop')
os_group = create(:group, name: 'OpenSource')
- create(:project, group: os_group)
+ create(:empty_project, group: os_group)
@os_user1 = create(:user)
@os_user2 = create(:user)
os_group.add_owner(@os_user1)
diff --git a/features/steps/shared/admin.rb b/features/steps/shared/admin.rb
index fbaa408226e..ac0a1764147 100644
--- a/features/steps/shared/admin.rb
+++ b/features/steps/shared/admin.rb
@@ -2,7 +2,7 @@ module SharedAdmin
include Spinach::DSL
step 'there are projects in system' do
- 2.times { create(:project) }
+ 2.times { create(:project, :repository) }
end
step 'system has users' do
diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb
index fe6736dacd4..de119f2c6c0 100644
--- a/features/steps/shared/group.rb
+++ b/features/steps/shared/group.rb
@@ -40,7 +40,7 @@ module SharedGroup
user = User.find_by(name: username) || create(:user, name: username)
group = Group.find_by(name: groupname) || create(:group, name: groupname)
group.add_user(user, role)
- project ||= create(:project, namespace: group, path: "project#{@project_count}")
+ project ||= create(:project, :repository, namespace: group, path: "project#{@project_count}")
create(:closed_issue_event, project: project)
project.team << [user, :master]
@project_count += 1
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index b51152c79c6..7a6707a7dfb 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -3,19 +3,19 @@ module SharedProject
# Create a project without caring about what it's called
step "I own a project" do
- @project = create(:project, namespace: @user.namespace)
+ @project = create(:project, :repository, namespace: @user.namespace)
@project.team << [@user, :master]
end
step "project exists in some group namespace" do
@group = create(:group, name: 'some group')
- @project = create(:project, namespace: @group, public_builds: false)
+ @project = create(:project, :repository, namespace: @group, public_builds: false)
end
# Create a specific project called "Shop"
step 'I own project "Shop"' do
@project = Project.find_by(name: "Shop")
- @project ||= create(:project, name: "Shop", namespace: @user.namespace)
+ @project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace)
@project.team << [@user, :master]
end
@@ -40,7 +40,7 @@ module SharedProject
# Create another specific project called "Forum"
step 'I own project "Forum"' do
@project = Project.find_by(name: "Forum")
- @project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project')
+ @project ||= create(:project, :repository, name: "Forum", namespace: @user.namespace, path: 'forum_project')
@project.build_project_feature
@project.project_feature.save
@project.team << [@user, :master]
@@ -97,7 +97,7 @@ module SharedProject
step 'I should see project settings' do
expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project)
expect(page).to have_content("Project name")
- expect(page).to have_content("Feature Visibility")
+ expect(page).to have_content("Sharing & Permissions")
end
def current_project
@@ -121,7 +121,7 @@ module SharedProject
# ----------------------------------------
step 'archived project "Archive"' do
- create :project, :public, archived: true, name: 'Archive'
+ create(:project, :archived, :public, :repository, name: 'Archive')
end
step 'I should not see project "Archive"' do
@@ -144,7 +144,7 @@ module SharedProject
# ----------------------------------------
step 'private project "Enterprise"' do
- create :project, name: 'Enterprise'
+ create(:project, :private, :repository, name: 'Enterprise')
end
step 'I should see project "Enterprise"' do
@@ -156,7 +156,7 @@ module SharedProject
end
step 'internal project "Internal"' do
- create :project, :internal, name: 'Internal'
+ create(:project, :internal, :repository, name: 'Internal')
end
step 'I should see project "Internal"' do
@@ -168,7 +168,7 @@ module SharedProject
end
step 'public project "Community"' do
- create :project, :public, name: 'Community'
+ create(:project, :public, :repository, name: 'Community')
end
step 'I should see project "Community"' do
diff --git a/features/steps/user.rb b/features/steps/user.rb
index 59385a6ab59..271c9b097d4 100644
--- a/features/steps/user.rb
+++ b/features/steps/user.rb
@@ -38,6 +38,6 @@ class Spinach::Features::User < Spinach::FeatureSteps
end
def contributed_project
- @contributed_project ||= create(:project, :public)
+ @contributed_project ||= create(:empty_project, :public)
end
end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 4bbdf06a49c..b6e6820c3f4 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -78,6 +78,8 @@ module API
description: params[:description]
)
+ render_validation_error!(status) if status.invalid?
+
begin
case params[:state]
when 'pending'
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 885ce7d44bc..9f59939e9ae 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -268,6 +268,13 @@ module API
end
end
+ class IssuableTimeStats < Grape::Entity
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+ end
+
class ExternalIssue < Grape::Entity
expose :title
expose :id
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index eb2d370c68e..6b81fbf294e 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -86,6 +86,10 @@ module API
IssuesFinder.new(current_user, project_id: user_project.id).find(id)
end
+ def find_project_merge_request(id)
+ MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
+ end
+
def authenticate!
unauthorized! unless current_user
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 161269cbd41..fe016c1ec0a 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -89,6 +89,8 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
+ include TimeTrackingEndpoints
+
desc 'Get a list of project issues' do
success Entities::Issue
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 5d1fe22f2df..e77af4b7a0d 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -10,6 +10,8 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
+ include TimeTrackingEndpoints
+
helpers do
def handle_merge_request_errors!(errors)
if errors[:project_access].any?
@@ -96,7 +98,7 @@ module API
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
end
delete ":id/merge_requests/:merge_request_id" do
- merge_request = user_project.merge_requests.find_by(id: params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize!(:destroy_merge_request, merge_request)
merge_request.destroy
@@ -116,7 +118,7 @@ module API
success Entities::MergeRequest
end
get path do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
@@ -125,7 +127,7 @@ module API
success Entities::RepoCommit
end
get "#{path}/commits" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request.commits, with: Entities::RepoCommit
end
@@ -134,7 +136,7 @@ module API
success Entities::MergeRequestChanges
end
get "#{path}/changes" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
@@ -153,7 +155,7 @@ module API
:remove_source_branch
end
put path do
- merge_request = user_project.merge_requests.find(params.delete(:merge_request_id))
+ merge_request = find_project_merge_request(params.delete(:merge_request_id))
authorize! :update_merge_request, merge_request
mr_params = declared_params(include_missing: false)
@@ -180,7 +182,7 @@ module API
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
end
put "#{path}/merge" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
# Merge request can not be merged
# because user dont have permissions to push into target branch
@@ -216,7 +218,7 @@ module API
success Entities::MergeRequest
end
post "#{path}/cancel_merge_when_build_succeeds" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
@@ -233,7 +235,7 @@ module API
use :pagination
end
get "#{path}/comments" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
@@ -248,7 +250,7 @@ module API
requires :note, type: String, desc: 'The text of the comment'
end
post "#{path}/comments" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :create_note, merge_request
opts = {
@@ -273,7 +275,7 @@ module API
use :pagination
end
get "#{path}/closes_issues" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: issue_entity(user_project), current_user: current_user
end
diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb
new file mode 100644
index 00000000000..85b5f7d98b8
--- /dev/null
+++ b/lib/api/time_tracking_endpoints.rb
@@ -0,0 +1,114 @@
+module API
+ module TimeTrackingEndpoints
+ extend ActiveSupport::Concern
+
+ included do
+ helpers do
+ def issuable_name
+ declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
+ end
+
+ def issuable_key
+ "#{issuable_name}_id".to_sym
+ end
+
+ def update_issuable_key
+ "update_#{issuable_name}".to_sym
+ end
+
+ def read_issuable_key
+ "read_#{issuable_name}".to_sym
+ end
+
+ def load_issuable
+ @issuable ||= begin
+ case issuable_name
+ when 'issue'
+ find_project_issue(params.delete(issuable_key))
+ when 'merge_request'
+ find_project_merge_request(params.delete(issuable_key))
+ end
+ end
+ end
+
+ def update_issuable(attrs)
+ custom_params = declared_params(include_missing: false)
+ custom_params.merge!(attrs)
+
+ issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable)
+ if issuable.valid?
+ present issuable, with: Entities::IssuableTimeStats
+ else
+ render_validation_error!(issuable)
+ end
+ end
+
+ def update_service
+ issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService
+ end
+ end
+
+ issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request'
+ issuable_collection_name = issuable_name.pluralize
+ issuable_key = "#{issuable_name}_id".to_sym
+
+ desc "Set a time estimate for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ requires :duration, type: String, desc: 'The duration to be parsed'
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)))
+ end
+
+ desc "Reset the time estimate for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(time_estimate: 0)
+ end
+
+ desc "Add spent time for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ requires :duration, type: String, desc: 'The duration to be parsed'
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do
+ authorize! update_issuable_key, load_issuable
+
+ update_issuable(spend_time: {
+ duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
+ user: current_user
+ })
+ end
+
+ desc "Reset spent time for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(spend_time: { duration: :reset, user: current_user })
+ end
+
+ desc "Show time stats for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do
+ authorize! read_issuable_key, load_issuable
+
+ present load_issuable, with: Entities::IssuableTimeStats
+ end
+ end
+ end
+end
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)
diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb
new file mode 100644
index 00000000000..4969a350862
--- /dev/null
+++ b/lib/gitlab/ci/status/external/common.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ module Status
+ module External
+ module Common
+ def has_details?
+ subject.target_url.present? &&
+ can?(user, :read_commit_status, subject)
+ end
+
+ def details_path
+ subject.target_url
+ end
+
+ def has_action?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/external/factory.rb b/lib/gitlab/ci/status/external/factory.rb
new file mode 100644
index 00000000000..07b15bd8d97
--- /dev/null
+++ b/lib/gitlab/ci/status/external/factory.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Ci
+ module Status
+ module External
+ class Factory < Status::Factory
+ def self.common_helpers
+ Status::External::Common
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
similarity index 59%
rename from lib/gitlab/cycle_analytics/base_event.rb
rename to lib/gitlab/cycle_analytics/base_event_fetcher.rb
index 53a148ad703..0d8791d396b 100644
--- a/lib/gitlab/cycle_analytics/base_event.rb
+++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
@@ -1,13 +1,13 @@
module Gitlab
module CycleAnalytics
- class BaseEvent
- include MetricsTables
+ class BaseEventFetcher
+ include BaseQuery
- attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query
+ attr_reader :projections, :query, :stage, :order
- def initialize(project:, options:)
- @query = EventsQuery.new(project: project, options: options)
+ def initialize(project:, stage:, options:)
@project = project
+ @stage = stage
@options = options
end
@@ -19,10 +19,8 @@ module Gitlab
end.compact
end
- def custom_query(_base_query); end
-
def order
- @order || @start_time_attrs
+ @order || default_order
end
private
@@ -34,7 +32,17 @@ module Gitlab
end
def event_result
- @event_result ||= @query.execute(self).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 default_order
+ [@options[:start_time_attrs]].flatten.first
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
new file mode 100644
index 00000000000..74bbcdcb3dd
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ module CycleAnalytics
+ class BaseStage
+ include BaseQuery
+
+ def initialize(project:, options:)
+ @project = project
+ @options = options
+ end
+
+ def events
+ event_fetcher.fetch
+ end
+
+ def as_json
+ AnalyticsStageSerializer.new.represent(self).as_json
+ end
+
+ def title
+ name.to_s.capitalize
+ end
+
+ def 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.dup, 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 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)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
similarity index 76%
rename from lib/gitlab/cycle_analytics/review_event.rb
rename to lib/gitlab/cycle_analytics/code_event_fetcher.rb
index b394a02cc52..5245b9ca8fc 100644
--- a/lib/gitlab/cycle_analytics/review_event.rb
+++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
@@ -1,22 +1,22 @@
module Gitlab
module CycleAnalytics
- class ReviewEvent < BaseEvent
+ class CodeEventFetcher < BaseEventFetcher
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],
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
diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb
new file mode 100644
index 00000000000..d1bc2055ba8
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/code_stage.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module CycleAnalytics
+ class CodeStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_table[:created_at]
+ end
+
+ def name
+ :code
+ end
+
+ def description
+ "Time until first merge request"
+ 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/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/events_query.rb b/lib/gitlab/cycle_analytics/events_query.rb
deleted file mode 100644
index 2418832ccc2..00000000000
--- a/lib/gitlab/cycle_analytics/events_query.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-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)
- 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/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb
deleted file mode 100644
index 705b7e5ce24..00000000000
--- a/lib/gitlab/cycle_analytics/issue_event.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class IssueEvent < BaseEvent
- 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]]
- @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.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
similarity index 72%
rename from lib/gitlab/cycle_analytics/production_event.rb
rename to lib/gitlab/cycle_analytics/issue_event_fetcher.rb
index 4868c3c6237..0d8da99455e 100644
--- a/lib/gitlab/cycle_analytics/production_event.rb
+++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
@@ -1,12 +1,9 @@
module Gitlab
module CycleAnalytics
- class ProductionEvent < BaseEvent
+ class IssueEventFetcher < BaseEventFetcher
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],
issue_table[:iid],
issue_table[:id],
diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb
new file mode 100644
index 00000000000..d2068fbc38f
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/issue_stage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module CycleAnalytics
+ class IssueStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= issue_table[:created_at]
+ end
+
+ 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
+ :issue
+ end
+
+ def description
+ "Time before an issue gets scheduled"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb
deleted file mode 100644
index b71e8735e27..00000000000
--- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class MetricsFetcher
- include Gitlab::Database::Median
- include Gitlab::Database::DateTime
- include MetricsTables
-
- DEPLOYMENT_METRIC_STAGES = %i[production staging]
-
- def initialize(project:, from:, branch:)
- @project = project
- @project = project
- @from = from
- @branch = branch
- end
-
- def calculate_metric(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).
- # 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(name), start_time_attrs, end_time_attrs, name.to_s))
-
- median_datetime(cte_table, interval_query, name)
- end
-
- # Join table with a row for every