summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock3
-rw-r--r--app/assets/javascripts/pipelines.js.es618
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss6
-rw-r--r--app/assets/stylesheets/framework/page-header.scss67
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/framework/wells.scss45
-rw-r--r--app/assets/stylesheets/pages/admin.scss6
-rw-r--r--app/assets/stylesheets/pages/builds.scss29
-rw-r--r--app/assets/stylesheets/pages/commit.scss142
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss3
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb7
-rw-r--r--app/controllers/concerns/issuable_collections.rb4
-rw-r--r--app/controllers/concerns/issues_action.rb1
-rw-r--r--app/controllers/concerns/merge_requests_action.rb1
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb65
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb11
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/helpers/environment_helper.rb29
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/models/ci/build.rb30
-rw-r--r--app/models/cycle_analytics.rb64
-rw-r--r--app/models/merge_request/metrics.rb1
-rw-r--r--app/models/project_services/jira_service.rb114
-rw-r--r--app/models/repository.rb13
-rw-r--r--app/serializers/analytics_build_entity.rb40
-rw-r--r--app/serializers/analytics_build_serializer.rb3
-rw-r--r--app/serializers/analytics_commit_entity.rb13
-rw-r--r--app/serializers/analytics_commit_serializer.rb3
-rw-r--r--app/serializers/analytics_generic_serializer.rb7
-rw-r--r--app/serializers/analytics_issue_entity.rb29
-rw-r--r--app/serializers/analytics_issue_serializer.rb3
-rw-r--r--app/serializers/analytics_merge_request_entity.rb7
-rw-r--r--app/serializers/analytics_merge_request_serializer.rb3
-rw-r--r--app/serializers/entity_date_helper.rb35
-rw-r--r--app/services/destroy_group_service.rb10
-rw-r--r--app/views/admin/builds/index.html.haml2
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/groups/edit.html.haml2
-rw-r--r--app/views/admin/groups/new.html.haml2
-rw-r--r--app/views/projects/builds/_header.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml24
-rw-r--r--app/views/projects/commit/_commit_box.html.haml13
-rw-r--r--app/views/projects/commit/_pipeline.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml7
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml3
-rw-r--r--app/views/projects/pipelines/_info.html.haml68
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml51
-rw-r--r--app/views/projects/pipelines/show.html.haml8
-rw-r--r--app/views/shared/icons/_icon_status_skipped.svg2
-rw-r--r--app/workers/build_success_worker.rb4
-rw-r--r--app/workers/pipeline_metrics_worker.rb4
-rw-r--r--changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml4
-rw-r--r--changelogs/unreleased/23223-group-deletion-race-condition.yml4
-rw-r--r--changelogs/unreleased/23637-title-bar-pipelines.yml4
-rw-r--r--changelogs/unreleased/bugfix-html-only-mail.yml4
-rw-r--r--changelogs/unreleased/changelog-update.yml4
-rw-r--r--changelogs/unreleased/feature-cycle-analytics-events.yml4
-rw-r--r--changelogs/unreleased/fix-admin-ci-table.yml4
-rw-r--r--changelogs/unreleased/issue_13232.yml4
-rw-r--r--changelogs/unreleased/optimize-mr-index.yml4
-rw-r--r--changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml4
-rw-r--r--config/no_todos_messages.yml12
-rw-r--r--config/routes/project.rb12
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb2
-rw-r--r--db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb33
-rw-r--r--db/migrate/20161117114805_remove_undeleted_groups.rb16
-rw-r--r--db/schema.rb5
-rw-r--r--doc/api/projects.md4
-rw-r--r--doc/ci/examples/test-scala-application.md41
-rw-r--r--doc/ci/yaml/README.md9
-rw-r--r--lib/gitlab/cycle_analytics/base_event.rb57
-rw-r--r--lib/gitlab/cycle_analytics/code_event.rb28
-rw-r--r--lib/gitlab/cycle_analytics/events.rb38
-rw-r--r--lib/gitlab/cycle_analytics/events_query.rb37
-rw-r--r--lib/gitlab/cycle_analytics/issue_allowed.rb9
-rw-r--r--lib/gitlab/cycle_analytics/issue_event.rb27
-rw-r--r--lib/gitlab/cycle_analytics/merge_request_allowed.rb9
-rw-r--r--lib/gitlab/cycle_analytics/metrics_fetcher.rb60
-rw-r--r--lib/gitlab/cycle_analytics/metrics_tables.rb37
-rw-r--r--lib/gitlab/cycle_analytics/plan_event.rb44
-rw-r--r--lib/gitlab/cycle_analytics/production_event.rb26
-rw-r--r--lib/gitlab/cycle_analytics/review_event.rb25
-rw-r--r--lib/gitlab/cycle_analytics/staging_event.rb31
-rw-r--r--lib/gitlab/cycle_analytics/test_event.rb13
-rw-r--r--lib/gitlab/cycle_analytics/updater.rb30
-rw-r--r--lib/gitlab/database/date_time.rb28
-rw-r--r--lib/gitlab/email/html_parser.rb34
-rw-r--r--lib/gitlab/email/reply_parser.rb19
-rw-r--r--spec/features/admin/admin_groups_spec.rb35
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb2
-rw-r--r--spec/features/projects/pipelines_spec.rb3
-rw-r--r--spec/fixtures/emails/outlook_html.eml140
-rw-r--r--spec/lib/gitlab/cycle_analytics/code_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb326
-rw-r--r--spec/lib/gitlab/cycle_analytics/issue_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/plan_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/production_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/review_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/shared_event_spec.rb21
-rw-r--r--spec/lib/gitlab/cycle_analytics/staging_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/test_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/updater_spec.rb25
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb4
-rw-r--r--spec/lib/light_url_builder_spec.rb119
-rw-r--r--spec/models/build_spec.rb128
-rw-r--r--spec/models/ci/build_spec.rb6
-rw-r--r--spec/models/project_services/jira_service_spec.rb44
-rw-r--r--spec/models/repository_spec.rb22
-rw-r--r--spec/requests/api/internal_spec.rb2
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb140
-rw-r--r--spec/serializers/analytics_build_entity_spec.rb27
-rw-r--r--spec/serializers/analytics_build_serializer_spec.rb22
-rw-r--r--spec/serializers/analytics_generic_entity_spec.rb39
-rw-r--r--spec/serializers/analytics_issue_serializer_spec.rb33
-rw-r--r--spec/serializers/analytics_merge_request_serializer_spec.rb34
-rw-r--r--spec/serializers/entity_date_helper_spec.rb45
-rw-r--r--spec/services/destroy_group_service_spec.rb40
-rw-r--r--spec/services/git_push_service_spec.rb10
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb6
-rw-r--r--spec/services/system_note_service_spec.rb45
-rw-r--r--spec/spec_helper.rb7
-rw-r--r--spec/support/database_connection_helpers.rb9
-rw-r--r--spec/support/jira_service_helper.rb5
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb133
-rw-r--r--spec/workers/pipeline_metrics_worker_spec.rb18
129 files changed, 2921 insertions, 393 deletions
diff --git a/Gemfile b/Gemfile
index 327d35c2350..9e815925a1f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -330,6 +330,7 @@ gem 'octokit', '~> 4.3.0'
gem 'mail_room', '~> 0.9.0'
gem 'email_reply_parser', '~> 0.5.8'
+gem 'html2text'
gem 'ruby-prof', '~> 0.16.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 4532f16cfe0..bdc60552480 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -339,6 +339,8 @@ GEM
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
+ html2text (0.2.0)
+ nokogiri (~> 1.6)
htmlentities (4.3.4)
httparty (0.13.7)
json (~> 1.8)
@@ -873,6 +875,7 @@ DEPENDENCIES
health_check (~> 2.2.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
+ html2text
httparty (~> 0.13.3)
influxdb (~> 0.2)
jira-ruby (~> 1.1.2)
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index e6fada5c84c..a84db9c0233 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -3,26 +3,12 @@
class Pipelines {
constructor() {
- this.initGraphToggle();
this.addMarginToBuildColumns();
}
- initGraphToggle() {
- this.pipelineGraph = document.querySelector('.pipeline-graph');
- this.toggleButton = document.querySelector('.toggle-pipeline-btn');
- this.toggleButtonText = this.toggleButton.querySelector('.toggle-btn-text');
- this.toggleButton.addEventListener('click', this.toggleGraph.bind(this));
- }
-
- toggleGraph() {
- const graphCollapsed = this.pipelineGraph.classList.contains('graph-collapsed');
- this.toggleButton.classList.toggle('graph-collapsed');
- this.pipelineGraph.classList.toggle('graph-collapsed');
- this.toggleButtonText.textContent = graphCollapsed ? 'Hide' : 'Expand';
- }
-
addMarginToBuildColumns() {
- const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
+ this.pipelineGraph = document.querySelector('.pipeline-graph');
+ const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)');
for (buildNodeIndex in secondChildBuildNodes) {
const buildNode = secondChildBuildNodes[buildNodeIndex];
const firstChildBuildNode = buildNode.previousElementSibling;
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index d5cca1b10fb..7c7f991dd87 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -39,3 +39,5 @@
@import "framework/typography.scss";
@import "framework/zen.scss";
@import "framework/blank";
+@import "framework/wells.scss";
+@import "framework/page-header.scss";
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 9acff45de75..4a9aa0f8717 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -349,6 +349,12 @@
}
}
+.btn-inverted {
+ &-secondary {
+ @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light);
+ }
+}
+
@media (max-width: $screen-xs-max) {
.btn-wide-on-xs {
width: 100%;
diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss
new file mode 100644
index 00000000000..85c1385d5d9
--- /dev/null
+++ b/app/assets/stylesheets/framework/page-header.scss
@@ -0,0 +1,67 @@
+.page-content-header {
+ line-height: 34px;
+ padding: 10px 0;
+ margin-bottom: 0;
+
+ @media (min-width: $screen-sm-min) {
+ display: flex;
+ align-items: center;
+
+ .header-main-content {
+ flex: 1;
+ }
+ }
+
+ .header-action-buttons {
+ i {
+ color: $gl-icon-color;
+ font-size: 13px;
+ margin-right: 3px;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ .btn {
+ width: 100%;
+ margin-top: 10px;
+ }
+
+ .dropdown {
+ width: 100%;
+ }
+ }
+ }
+
+ .avatar {
+ @extend .avatar-inline;
+ margin-left: 0;
+
+ @media (min-width: $screen-sm-min) {
+ margin-left: 4px;
+ }
+ }
+
+ .commit-committer-link,
+ .commit-author-link {
+ color: $gl-gray;
+ font-weight: bold;
+ }
+
+ .fa-clipboard {
+ color: $dropdown-title-btn-color;
+ }
+
+ .commit-info {
+ &.branches {
+ margin-left: 8px;
+ }
+ }
+
+ .ci-status-link {
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin: 0 2px 0 3px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 8bf5edfde50..92226f7432e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -90,8 +90,8 @@ $table-border-color: #f0f0f0;
$background-color: $gray-light;
$dark-background-color: #f5f5f5;
$table-text-gray: #8f8f8f;
-$widget-expand-item: #e8f2f7;
-$widget-inner-border: #eef0f2;
+$well-expand-item: #e8f2f7;
+$well-inner-border: #eef0f2;
/*
* Text
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
new file mode 100644
index 00000000000..192939f4527
--- /dev/null
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -0,0 +1,45 @@
+.info-well {
+ background: $background-color;
+ color: $gl-gray;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+
+ .well-segment {
+ padding: $gl-padding;
+
+ &:not(:last-of-type) {
+ border-bottom: 1px solid $well-inner-border;
+ }
+
+ &.branch-info {
+ .monospace,
+ .commit-info {
+ margin-left: 4px;
+ }
+ }
+ }
+
+ .icon-container {
+ display: inline-block;
+ margin-right: 8px;
+
+ svg {
+ position: relative;
+ top: 2px;
+ height: 16px;
+ width: 16px;
+ }
+
+ &.commit-icon {
+ svg {
+ path {
+ fill: $gl-text-color;
+ }
+ }
+ }
+ }
+
+ .label.label-gray {
+ background-color: $well-expand-item;
+ }
+}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 6cefafd8fc7..14812e171fd 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -160,3 +160,9 @@
}
}
}
+
+.admin-builds-table {
+ .ci-table td:last-child {
+ min-width: 120px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index f1d311cabbe..48f11eb2552 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -40,6 +40,19 @@
margin-bottom: 10px;
}
}
+
+ .environment-information {
+ background-color: $background-color;
+ border: 1px solid $border-color;
+ padding: 12px $gl-padding;
+ border-radius: $border-radius-default;
+
+ svg {
+ position: relative;
+ top: 1px;
+ margin-right: 5px;
+ }
+ }
}
.build-header {
@@ -49,10 +62,6 @@
min-height: 58px;
align-items: center;
- .btn-inverted {
- @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light);
- }
-
@media (max-width: $screen-sm-max) {
padding-right: 40px;
@@ -63,14 +72,14 @@
.header-content {
flex: 1;
- }
- a {
- color: $gl-gray;
+ a {
+ color: $gl-gray;
- &:hover {
- color: $gl-link-color;
- text-decoration: none;
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 47d3e72679b..ddc9d0e2b1a 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -26,143 +26,12 @@
white-space: pre-wrap;
}
-.commit-info-row {
- margin-bottom: 10px;
- line-height: 24px;
- padding-top: 6px;
-
- &.commit-info-row-header {
- line-height: 34px;
- padding: 10px 0;
- margin-bottom: 0;
-
- @media (min-width: $screen-sm-min) {
- display: flex;
- align-items: center;
-
- .commit-meta {
- flex: 1;
- }
- }
-
- .commit-hash-full {
- @media (max-width: $screen-sm-max) {
- width: 80px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: inline-block;
- vertical-align: bottom;
- }
- }
-
- .commit-action-buttons {
- i {
- color: $gl-icon-color;
- font-size: 13px;
- margin-right: 3px;
- }
-
- @media (max-width: $screen-xs-max) {
- .dropdown {
- width: 100%;
- margin-top: 10px;
- }
-
- .dropdown-toggle {
- width: 100%;
- }
- }
- }
- }
-
- .avatar {
- @extend .avatar-inline;
- margin-left: 0;
-
- @media (min-width: $screen-sm-min) {
- margin-left: 4px;
- }
- }
-
- .commit-committer-link,
- .commit-author-link {
- color: $gl-gray;
- font-weight: bold;
- }
-
- .fa-clipboard {
- color: $dropdown-title-btn-color;
- }
-
- .commit-info {
- &.branches {
- margin-left: 8px;
- }
- }
-
- .ci-status-link {
-
- svg {
- position: relative;
- top: 2px;
- margin: 0 2px 0 3px;
- }
- }
-}
-
.js-details-expand {
&:hover {
text-decoration: none;
}
}
-.commit-info-widget {
- background: $background-color;
- color: $gl-gray;
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
-
- .widget-row {
- padding: $gl-padding;
-
- &:not(:last-of-type) {
- border-bottom: 1px solid $widget-inner-border;
- }
-
- &.branch-info {
- .monospace,
- .commit-info {
- margin-left: 4px;
- }
- }
- }
-
- .icon-container {
- display: inline-block;
- margin-right: 8px;
-
- svg {
- position: relative;
- top: 2px;
- height: 16px;
- width: 16px;
- }
-
- &.commit-icon {
- svg {
- path {
- fill: $gl-text-color;
- }
- }
- }
- }
-
- .label.label-gray {
- background-color: $widget-expand-item;
- }
-}
-
.ci-status-link {
svg {
overflow: visible;
@@ -184,6 +53,17 @@
}
}
+.commit-hash-full {
+ @media (max-width: $screen-sm-max) {
+ width: 80px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ vertical-align: bottom;
+ }
+}
+
.file-stats {
ul {
list-style: none;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 6cf43713fec..b6a82460f25 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -61,7 +61,7 @@
}
.ci_widget {
- border-bottom: 1px solid $widget-inner-border;
+ border-bottom: 1px solid $well-inner-border;
svg {
margin-right: 4px;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 881621a2655..a44a496c728 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -300,6 +300,8 @@
.pipeline-graph {
width: 100%;
+ background-color: $background-color;
+ padding: $gl-padding;
overflow: auto;
white-space: nowrap;
transition: max-height 0.3s, padding 0.3s;
@@ -363,6 +365,7 @@
.build {
border: 1px solid $border-color;
+ background-color: $white-light;
position: relative;
padding: 7px 10px 8px;
border-radius: 30px;
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index fcfa508128e..5c44637fdee 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -55,7 +55,7 @@ class AutocompleteController < ApplicationController
def find_users
@users =
if @project
- user_ids = @project.team.users.map(&:id)
+ user_ids = @project.team.users.pluck(:id)
if params[:author_id].present?
user_ids << params[:author_id]
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
new file mode 100644
index 00000000000..2aaf8f2b451
--- /dev/null
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -0,0 +1,7 @@
+module CycleAnalyticsParams
+ extend ActiveSupport::Concern
+
+ def start_date(params)
+ params[:start_date] == '30' ? 30.days.ago : 90.days.ago
+ end
+end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index b5e79099e39..6247934f81e 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -10,11 +10,11 @@ module IssuableCollections
private
def issues_collection
- issues_finder.execute
+ issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
end
def merge_requests_collection
- merge_requests_finder.execute
+ merge_requests_finder.execute.preload(:source_project, :target_project, :author, :assignee, :labels, :milestone, :merge_request_diff, target_project: :namespace)
end
def issues_finder
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index b89fb94be6e..b46adcceb60 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -7,7 +7,6 @@ module IssuesAction
@issues = issues_collection
.non_archived
- .preload(:author, :project)
.page(params[:page])
respond_to do |format|
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index a1b0eee37f9..6546a07b41c 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -7,7 +7,6 @@ module MergeRequestsAction
@merge_requests = merge_requests_collection
.non_archived
- .preload(:author, :target_project)
.page(params[:page])
end
end
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
new file mode 100644
index 00000000000..13b3eec761f
--- /dev/null
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -0,0 +1,65 @@
+module Projects
+ module CycleAnalytics
+ class EventsController < Projects::ApplicationController
+ include CycleAnalyticsParams
+
+ before_action :authorize_read_cycle_analytics!
+ before_action :authorize_read_build!, only: [:test, :staging]
+ before_action :authorize_read_issue!, only: [:issue, :production]
+ before_action :authorize_read_merge_request!, only: [:code, :review]
+
+ def issue
+ render_events(events.issue_events)
+ end
+
+ def plan
+ render_events(events.plan_events)
+ end
+
+ def code
+ render_events(events.code_events)
+ end
+
+ def test
+ options[:branch] = events_params[:branch_name]
+
+ render_events(events.test_events)
+ end
+
+ def review
+ render_events(events.review_events)
+ end
+
+ def staging
+ render_events(events.staging_events)
+ end
+
+ def production
+ render_events(events.production_events)
+ end
+
+ private
+
+ def render_events(events_list)
+ respond_to do |format|
+ format.html
+ format.json { render json: { events: events_list } }
+ end
+ end
+
+ def events
+ @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?
+
+ params[:events].slice(:start_date, :branch_name)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index 16a7b1fc6e2..96eb75a0547 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -1,11 +1,12 @@
class Projects::CycleAnalyticsController < Projects::ApplicationController
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TextHelper
+ include CycleAnalyticsParams
before_action :authorize_read_cycle_analytics!
def show
- @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date)
+ @cycle_analytics = ::CycleAnalytics.new(@project, from: start_date(cycle_analytics_params))
respond_to do |format|
format.html
@@ -15,14 +16,6 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
private
- def parse_start_date
- case cycle_analytics_params[:start_date]
- when '30' then 30.days.ago
- when '90' then 90.days.ago
- else 90.days.ago
- end
- end
-
def cycle_analytics_params
return {} unless params[:cycle_analytics].present?
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index dff0213411c..036fde87619 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -38,7 +38,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def index
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
- @merge_requests = @merge_requests.preload(:target_project)
if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] }
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
new file mode 100644
index 00000000000..27975b7ddb7
--- /dev/null
+++ b/app/helpers/environment_helper.rb
@@ -0,0 +1,29 @@
+module EnvironmentHelper
+ def environment_for_build(project, build)
+ return unless build.environment
+
+ project.environments.find_by(name: build.expanded_environment_name)
+ end
+
+ def environment_link_for_build(project, build)
+ environment = environment_for_build(project, build)
+ if environment
+ link_to environment.name, namespace_project_environment_path(project.namespace, project, environment)
+ else
+ content_tag :span, build.expanded_environment_name
+ end
+ end
+
+ def deployment_link(deployment)
+ return unless deployment
+
+ link_to "##{deployment.iid}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+ end
+
+ def last_deployment_link_for_environment_build(project, build)
+ environment = environment_for_build(project, build)
+ return unless environment
+
+ deployment_link(environment.last_deployment)
+ end
+end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index ce2cabd7a3a..8bebda07787 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -171,9 +171,11 @@ module IssuablesHelper
def issuables_count_for_state(issuable_type, state)
issuables_finder = public_send("#{issuable_type}_finder")
- issuables_finder.params[:state] = state
+
+ params = issuables_finder.params.merge(state: state)
+ finder = issuables_finder.class.new(issuables_finder.current_user, params)
- issuables_finder.execute.page(1).total_count
+ finder.execute.page(1).total_count
end
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 33612256540..5d2e7d94190 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -7,6 +7,8 @@ module Ci
belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User'
+ has_many :deployments, as: :deployable
+
serialize :options
serialize :yaml_variables
@@ -125,6 +127,34 @@ module Ci
!self.pipeline.statuses.latest.include?(self)
end
+ def expanded_environment_name
+ ExpandVariables.expand(environment, variables) if environment
+ end
+
+ def has_environment?
+ self.environment.present?
+ end
+
+ def starts_environment?
+ has_environment? && self.environment_action == 'start'
+ end
+
+ def stops_environment?
+ has_environment? && self.environment_action == 'stop'
+ end
+
+ def environment_action
+ self.options.fetch(:environment, {}).fetch(:action, 'start')
+ end
+
+ def outdated_deployment?
+ success? && !last_deployment.try(:last?)
+ end
+
+ def last_deployment
+ deployments.last
+ end
+
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
index 8ed4a56b19b..314a1ce9b63 100644
--- a/app/models/cycle_analytics.rb
+++ b/app/models/cycle_analytics.rb
@@ -1,12 +1,8 @@
class CycleAnalytics
- include Gitlab::Database::Median
- include Gitlab::Database::DateTime
-
- DEPLOYMENT_METRIC_STAGES = %i[production staging]
-
def initialize(project, from:)
@project = project
@from = from
+ @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil)
end
def summary
@@ -14,90 +10,46 @@ class CycleAnalytics
end
def issue
- calculate_metric(: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]])
end
def plan
- calculate_metric(: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
- calculate_metric(:code,
+ @fetcher.calculate_metric(:code,
Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
MergeRequest.arel_table[:created_at])
end
def test
- calculate_metric(:test,
+ @fetcher.calculate_metric(:test,
MergeRequest::Metrics.arel_table[:latest_build_started_at],
MergeRequest::Metrics.arel_table[:latest_build_finished_at])
end
def review
- calculate_metric(:review,
+ @fetcher.calculate_metric(:review,
MergeRequest.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:merged_at])
end
def staging
- calculate_metric(:staging,
+ @fetcher.calculate_metric(:staging,
MergeRequest::Metrics.arel_table[:merged_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end
def production
- calculate_metric(:production,
+ @fetcher.calculate_metric(:production,
Issue.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end
-
- private
-
- 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), end_time_attrs, start_time_attrs, name.to_s))
-
- median_datetime(cte_table, interval_query, name)
- end
-
- # Join table with a row for every <issue,merge_request> 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)
- arel_table = MergeRequestsClosingIssues.arel_table
-
- # Load issues
- query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])).
- join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])).
- where(Issue.arel_table[:project_id].eq(@project.id)).
- where(Issue.arel_table[:deleted_at].eq(nil)).
- where(Issue.arel_table[:created_at].gteq(@from))
-
- # Load merge_requests
- query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin).
- on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])).
- join(MergeRequest::Metrics.arel_table).
- on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
-
- if DEPLOYMENT_METRIC_STAGES.include?(name)
- # Limit to merge requests that have been deployed to production after `@from`
- query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
- end
-
- query
- end
end
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index 99c49a020c9..cdc408738be 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -1,5 +1,6 @@
class MergeRequest::Metrics < ActiveRecord::Base
belongs_to :merge_request
+ belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
def record!
if merge_request.merged? && self.merged_at.blank?
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 7ce274b5dca..8915c06b633 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -70,7 +70,7 @@ class JiraService < IssueTrackerService
end
def jira_project
- @jira_project ||= client.Project.find(project_key)
+ @jira_project ||= jira_request { client.Project.find(project_key) }
end
def help
@@ -128,12 +128,19 @@ class JiraService < IssueTrackerService
# we just want to test settings
test_settings
else
- close_issue(push, issue)
+ jira_issue = jira_request { client.Issue.find(issue.iid) }
+
+ return false unless jira_issue.present?
+
+ close_issue(push, jira_issue)
end
end
def create_cross_reference_note(mentioned, noteable, author)
- issue_key = mentioned.id
+ jira_issue = jira_request { client.Issue.find(mentioned.id) }
+
+ return false unless jira_issue.present?
+
project = self.project
noteable_name = noteable.class.name.underscore.downcase
noteable_id = if noteable.is_a?(Commit)
@@ -160,7 +167,7 @@ class JiraService < IssueTrackerService
}
}
- add_comment(data, issue_key)
+ add_comment(data, jira_issue)
end
# reason why service cannot be tested
@@ -181,16 +188,14 @@ class JiraService < IssueTrackerService
def test_settings
return unless url.present?
# Test settings by getting the project
- jira_project
-
- rescue Errno::ECONNREFUSED, JIRA::HTTPError => e
- Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{url}."
- false
+ jira_request { jira_project.present? }
end
private
def close_issue(entity, issue)
+ return if issue.nil? || issue.resolution.present?
+
commit_id = if entity.is_a?(Commit)
entity.id
elsif entity.is_a?(MergeRequest)
@@ -200,55 +205,85 @@ class JiraService < IssueTrackerService
commit_url = build_entity_url(:commit, commit_id)
# Depending on the JIRA project's workflow, a comment during transition
- # may or may not be allowed. Split the operation in to two calls so the
- # comment always works.
- transition_issue(issue)
- add_issue_solved_comment(issue, commit_id, commit_url)
+ # may or may not be allowed. Refresh the issue after transition and check
+ # if it is closed, so we don't have one comment for every commit.
+ issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue)
+ add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution
end
def transition_issue(issue)
- issue = client.Issue.find(issue.iid)
issue.transitions.build.save(transition: { id: jira_issue_transition_id })
end
def add_issue_solved_comment(issue, commit_id, commit_url)
- comment = "Issue solved with [#{commit_id}|#{commit_url}]."
- send_message(issue.iid, comment)
+ link_title = "GitLab: Solved by commit #{commit_id}."
+ comment = "Issue solved with [#{commit_id}|#{commit_url}]."
+ link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true)
+ send_message(issue, comment, link_props)
end
- def add_comment(data, issue_key)
- user_name = data[:user][:name]
- user_url = data[:user][:url]
- entity_name = data[:entity][:name]
- entity_url = data[:entity][:url]
+ def add_comment(data, issue)
+ user_name = data[:user][:name]
+ user_url = data[:user][:url]
+ entity_name = data[:entity][:name]
+ entity_url = data[:entity][:url]
entity_title = data[:entity][:title]
project_name = data[:project][:name]
- message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
+ message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
+ link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}"
+ link_props = build_remote_link_props(url: entity_url, title: link_title)
- unless comment_exists?(issue_key, message)
- send_message(issue_key, message)
+ unless comment_exists?(issue, message)
+ send_message(issue, message, link_props)
end
end
- def comment_exists?(issue_key, message)
- comments = client.Issue.find(issue_key).comments
- comments.map { |comment| comment.body.include?(message) }.any?
+ def comment_exists?(issue, message)
+ comments = jira_request { issue.comments }
+
+ comments.present? && comments.any? { |comment| comment.body.include?(message) }
end
- def send_message(issue_key, message)
+ def send_message(issue, message, remote_link_props)
return unless url.present?
- issue = client.Issue.find(issue_key)
+ jira_request do
+ if issue.comments.build.save!(body: message)
+ remote_link = issue.remotelink.build
+ remote_link.save!(remote_link_props)
+ result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ end
- if issue.comments.build.save!(body: message)
- result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ Rails.logger.info(result_message)
+ result_message
end
+ end
- Rails.logger.info(result_message)
- result_message
- rescue URI::InvalidURIError, Errno::ECONNREFUSED, JIRA::HTTPError => e
- Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
+ # Build remote link on JIRA properties
+ # Icons here must be available on WEB so JIRA can read the URL
+ # We are using a open word graphics icon which have LGPL license
+ def build_remote_link_props(url:, title:, resolved: false)
+ status = {
+ resolved: resolved
+ }
+
+ if resolved
+ status[:icon] = {
+ title: 'Closed',
+ url16x16: 'http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png'
+ }
+ end
+
+ {
+ GlobalID: 'GitLab',
+ object: {
+ url: url,
+ title: title,
+ status: status,
+ icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' }
+ }
+ }
end
def resource_url(resource)
@@ -266,4 +301,13 @@ class JiraService < IssueTrackerService
host: Settings.gitlab.base_url
)
end
+
+ # Handle errors when doing JIRA API calls
+ def jira_request
+ yield
+
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError => e
+ Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
+ nil
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 146424d2b1c..31be06be50c 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -176,11 +176,18 @@ class Repository
options = { message: message, tagger: user_to_committer(user) } if message
- GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
- rugged.tags.create(tag_name, target, options)
+ rugged.tags.create(tag_name, target, options)
+ tag = find_tag(tag_name)
+
+ GitHooksService.new.execute(user, path_to_repo, oldrev, tag.target, ref) do
+ # we already created a tag, because we need tag SHA to pass correct
+ # values to hooks
end
- find_tag(tag_name)
+ tag
+ rescue GitHooksService::PreReceiveError
+ rugged.tags.delete(tag_name)
+ raise
end
def rm_branch(user, branch_name)
diff --git a/app/serializers/analytics_build_entity.rb b/app/serializers/analytics_build_entity.rb
new file mode 100644
index 00000000000..5fdf2bbf7c3
--- /dev/null
+++ b/app/serializers/analytics_build_entity.rb
@@ -0,0 +1,40 @@
+class AnalyticsBuildEntity < Grape::Entity
+ include RequestAwareEntity
+ include EntityDateHelper
+
+ expose :name
+ expose :id
+ expose :ref, as: :branch
+ expose :short_sha
+ expose :author, using: UserEntity
+
+ expose :started_at, as: :date do |build|
+ interval_in_words(build[:started_at])
+ end
+
+ expose :duration, as: :total_time do |build|
+ distance_of_time_as_hash(build[:duration].to_f)
+ end
+
+ expose :branch do
+ expose :ref, as: :name
+
+ expose :url do |build|
+ url_to(:namespace_project_tree, build, build.ref)
+ end
+ end
+
+ expose :url do |build|
+ url_to(:namespace_project_build, build)
+ end
+
+ expose :commit_url do |build|
+ url_to(:namespace_project_commit, build, build.sha)
+ end
+
+ private
+
+ def url_to(route, build, id = nil)
+ public_send("#{route}_url", build.project.namespace, build.project, id || build)
+ end
+end
diff --git a/app/serializers/analytics_build_serializer.rb b/app/serializers/analytics_build_serializer.rb
new file mode 100644
index 00000000000..f172d67d356
--- /dev/null
+++ b/app/serializers/analytics_build_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsBuildSerializer < BaseSerializer
+ entity AnalyticsBuildEntity
+end
diff --git a/app/serializers/analytics_commit_entity.rb b/app/serializers/analytics_commit_entity.rb
new file mode 100644
index 00000000000..402cecbfd08
--- /dev/null
+++ b/app/serializers/analytics_commit_entity.rb
@@ -0,0 +1,13 @@
+class AnalyticsCommitEntity < CommitEntity
+ include EntityDateHelper
+
+ expose :short_id, as: :short_sha
+
+ expose :total_time do |commit|
+ distance_of_time_as_hash(request.total_time.to_f)
+ end
+
+ unexpose :author_name
+ unexpose :author_email
+ unexpose :message
+end
diff --git a/app/serializers/analytics_commit_serializer.rb b/app/serializers/analytics_commit_serializer.rb
new file mode 100644
index 00000000000..cdbfecf2b70
--- /dev/null
+++ b/app/serializers/analytics_commit_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsCommitSerializer < BaseSerializer
+ entity AnalyticsCommitEntity
+end
diff --git a/app/serializers/analytics_generic_serializer.rb b/app/serializers/analytics_generic_serializer.rb
new file mode 100644
index 00000000000..9f4859e8410
--- /dev/null
+++ b/app/serializers/analytics_generic_serializer.rb
@@ -0,0 +1,7 @@
+class AnalyticsGenericSerializer < BaseSerializer
+ def represent(resource, opts = {})
+ resource.symbolize_keys!
+
+ super(resource, opts)
+ end
+end
diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb
new file mode 100644
index 00000000000..44c50f18613
--- /dev/null
+++ b/app/serializers/analytics_issue_entity.rb
@@ -0,0 +1,29 @@
+class AnalyticsIssueEntity < Grape::Entity
+ include RequestAwareEntity
+ include EntityDateHelper
+
+ expose :title
+ expose :author, using: UserEntity
+
+ expose :iid do |object|
+ object[:iid].to_s
+ end
+
+ expose :total_time do |object|
+ distance_of_time_as_hash(object[:total_time].to_f)
+ end
+
+ expose(:created_at) do |object|
+ interval_in_words(object[:created_at])
+ end
+
+ expose :url do |object|
+ url_to(:namespace_project_issue, id: object[:iid].to_s)
+ end
+
+ private
+
+ def url_to(route, id)
+ public_send("#{route}_url", request.project.namespace, request.project, id)
+ end
+end
diff --git a/app/serializers/analytics_issue_serializer.rb b/app/serializers/analytics_issue_serializer.rb
new file mode 100644
index 00000000000..4fb3e8f1bb4
--- /dev/null
+++ b/app/serializers/analytics_issue_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsIssueSerializer < AnalyticsGenericSerializer
+ entity AnalyticsIssueEntity
+end
diff --git a/app/serializers/analytics_merge_request_entity.rb b/app/serializers/analytics_merge_request_entity.rb
new file mode 100644
index 00000000000..888265eaa38
--- /dev/null
+++ b/app/serializers/analytics_merge_request_entity.rb
@@ -0,0 +1,7 @@
+class AnalyticsMergeRequestEntity < AnalyticsIssueEntity
+ expose :state
+
+ expose :url do |object|
+ url_to(:namespace_project_merge_request, id: object[:iid].to_s)
+ end
+end
diff --git a/app/serializers/analytics_merge_request_serializer.rb b/app/serializers/analytics_merge_request_serializer.rb
new file mode 100644
index 00000000000..4622a1dd855
--- /dev/null
+++ b/app/serializers/analytics_merge_request_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsMergeRequestSerializer < AnalyticsGenericSerializer
+ entity AnalyticsMergeRequestEntity
+end
diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb
new file mode 100644
index 00000000000..b333b3344c3
--- /dev/null
+++ b/app/serializers/entity_date_helper.rb
@@ -0,0 +1,35 @@
+module EntityDateHelper
+ include ActionView::Helpers::DateHelper
+
+ def interval_in_words(diff)
+ "#{distance_of_time_in_words(diff.to_f)} ago"
+ end
+
+ # Converts seconds into a hash such as:
+ # { days: 1, hours: 3, mins: 42, seconds: 40 }
+ #
+ # It returns 0 seconds for zero or negative numbers
+ # It rounds to nearest time unit and does not return zero
+ # i.e { min: 1 } instead of { mins: 1, seconds: 0 }
+ def distance_of_time_as_hash(diff)
+ diff = diff.abs.floor
+
+ return { seconds: 0 } if diff == 0
+
+ mins = (diff / 60).floor
+ seconds = diff % 60
+ hours = (mins / 60).floor
+ mins = mins % 60
+ days = (hours / 24).floor
+ hours = hours % 24
+
+ duration_hash = {}
+
+ duration_hash[:days] = days if days > 0
+ duration_hash[:hours] = hours if hours > 0
+ duration_hash[:mins] = mins if mins > 0
+ duration_hash[:seconds] = seconds if seconds > 0
+
+ duration_hash
+ end
+end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index 0081364b8aa..a880952e274 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -6,12 +6,10 @@ class DestroyGroupService
end
def async_execute
- group.transaction do
- # Soft delete via paranoia gem
- group.destroy
- job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
- Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
- end
+ # Soft delete via paranoia gem
+ group.destroy
+ job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
+ Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
end
def execute
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 26a8846b609..5e3f105d41f 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -14,5 +14,5 @@
.row-content-block.second-block
#{(@scope || 'all').capitalize} builds
- %ul.content-list.builds-content-list
+ %ul.content-list.builds-content-list.admin-builds-table
= render "projects/builds/table", builds: @builds, admin: true
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 817910f7ddf..589f4557b52 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -7,7 +7,7 @@
.col-sm-10
= render 'shared/choose_group_avatar_button', f: f
- = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
+ = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml
index eb09a6328ed..c2b9807015d 100644
--- a/app/views/admin/groups/edit.html.haml
+++ b/app/views/admin/groups/edit.html.haml
@@ -1,4 +1,4 @@
- page_title "Edit", @group.name, "Groups"
%h3.page-title Edit group: #{@group.name}
%hr
-= render 'form'
+= render 'form', visibility_level: @group.visibility_level
diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml
index c81ee552ac3..8f9fe96249f 100644
--- a/app/views/admin/groups/new.html.haml
+++ b/app/views/admin/groups/new.html.haml
@@ -1,4 +1,4 @@
- page_title "New Group"
%h3.page-title New group
%hr
-= render 'form'
+= render 'form', visibility_level: default_group_visibility
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 9f69bd64f71..f6aa20c4579 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -17,6 +17,6 @@
= render "user"
= time_ago_with_tooltip(@build.created_at)
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted pull-right', method: :post
+ = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index f533eec642e..d8cbfd7173a 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -26,6 +26,30 @@
= link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
Runners page
+ - if @build.starts_environment?
+ .prepend-top-default
+ .environment-information
+ - if @build.outdated_deployment?
+ = ci_icon_for_status('success_with_warnings')
+ - else
+ = ci_icon_for_status(@build.status)
+
+ - environment = environment_for_build(@build.project, @build)
+ - if @build.success? && @build.last_deployment.present?
+ - if @build.last_deployment.last?
+ This build is the most recent deployment to #{environment_link_for_build(@build.project, @build)}.
+ - else
+ This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
+ - if environment.last_deployment
+ View the most recent deployment #{deployment_link(environment.last_deployment)}.
+ - elsif @build.complete? && !@build.success?
+ The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not succeed.
+ - else
+ This build is creating a deployment to #{environment_link_for_build(@build.project, @build)}
+ - if environment.last_deployment
+ and will overwrite the
+ = link_to 'latest deployment', deployment_link(environment.last_deployment)
+
.prepend-top-default
- if @build.erased?
.erased.alert.alert-warning
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 0ebc38d16cf..503cbd13b5e 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,5 +1,5 @@
-.commit-info-row.commit-info-row-header
- .commit-meta
+.page-content-header
+ .header-main-content
%strong Commit
%strong.monospace.js-details-short= @commit.short_id
= link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
@@ -19,7 +19,8 @@
%strong
= commit_committer_link(@commit, avatar: true, size: 24)
#{time_ago_with_tooltip(@commit.committed_date)}
- .commit-action-buttons
+
+ .header-action-buttons
- if defined?(@notes_count) && @notes_count > 0
%span.btn.disabled.btn-grouped.hidden-xs.append-right-10
= icon('comment')
@@ -55,8 +56,8 @@
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
-.commit-info-widget
- .widget-row.branch-info
+.info-well
+ .well-segment.branch-info
.icon-container.commit-icon
= custom_icon("icon_commit")
%span.cgray= pluralize(@commit.parents.count, "parent")
@@ -66,7 +67,7 @@
%i.fa.fa-spinner.fa-spin
- if @commit.status
- .widget-row.pipeline-info
+ .well-segment.pipeline-info
.icon-container
= ci_icon_for_status(@commit.status)
Pipeline
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 062a8905a19..1174158eb65 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -1,10 +1,6 @@
.pipeline-graph-container
.row-content-block.build-content.middle-block.pipeline-actions
.pull-right
- %button.btn.btn-grouped.btn-white.toggle-pipeline-btn
- %span.toggle-btn-text Hide
- %span pipeline graph
- %span.caret
- if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 34855c54176..12096941209 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -36,7 +36,6 @@
%pre.commit-row-description.js-toggle-content
= preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
- .commit-row-info
- = commit_author_link(commit, avatar: false, size: 24)
- authored
- #{time_ago_with_tooltip(commit.committed_date)}
+ = commit_author_link(commit, avatar: false, size: 24)
+ authored
+ #{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 12408068834..9ffcc48eb80 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -54,15 +54,18 @@
= link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do
= icon('code-fork')
= merge_request.target_branch
+
- if merge_request.milestone
&nbsp;
= link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
= icon('clock-o')
= merge_request.milestone.title
+
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
= link_to_label(label, subject: merge_request.project, type: :merge_request)
+
- if merge_request.tasks?
&nbsp;
%span.task-status
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index d288efc546f..a0de125d765 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,39 +1,49 @@
-%p
-.commit-info-row
- Pipeline
- = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace"
- with
- = pluralize @pipeline.statuses.count(:id), "build"
- - if @pipeline.ref
- for
- = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
- - if @pipeline.duration
- in
- = time_interval_in_words(@pipeline.duration)
- - if @pipeline.queued_duration
- = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
-
- .pull-right
+.page-content-header
+ .header-main-content
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
= ci_icon_for_status(@pipeline.status)
= ci_label_for_status(@pipeline.status)
+ %strong Pipeline ##{@commit.pipelines.last.id}
+ triggered #{time_ago_with_tooltip(@commit.authored_date)} by
+ = author_avatar(@commit, size: 24)
+ = commit_author_link(@commit)
+ .header-action-buttons
+ - if can?(current_user, :update_pipeline, @pipeline.project)
+ - if @pipeline.builds.latest.failed.any?(&:retryable?)
+ = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-inverted-secondary', method: :post
+ - if @pipeline.builds.running_or_pending.any?
+ = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- if @commit
- .commit-info-row
- %span.light Authored by
- %strong
- = commit_author_link(@commit, avatar: true, size: 24)
- #{time_ago_with_tooltip(@commit.authored_date)}
-
-.commit-info-row
- %span.light Commit
- = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace"
- = clipboard_button(clipboard_text: @pipeline.sha)
-
-- if @commit
- .commit-box.content-block
+ .commit-box
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line)
- if @commit.description.present?
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line))
+
+.info-well
+ - if @commit.status
+ .well-segment.pipeline-info
+ .icon-container
+ = ci_icon_for_status(@commit.status)
+ = pluralize @pipeline.statuses.count(:id), "build"
+ - if @pipeline.ref
+ from
+ = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
+ - if @pipeline.duration
+ in
+ = time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+
+ .well-segment.branch-info
+ .icon-container.commit-icon
+ = custom_icon("icon_commit")
+ = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace js-details-short"
+ = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
+ %span.text-expander
+ \...
+ %span.js-details-content.hide
+ = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace commit-hash-full"
+ = clipboard_button(clipboard_text: @pipeline.sha, title: "Copy commit SHA to clipboard")
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
new file mode 100644
index 00000000000..718314701f9
--- /dev/null
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -0,0 +1,51 @@
+.tabs-holder
+ %ul.nav-links.no-top.no-bottom
+ %li.active
+ = link_to "Pipeline", "#js-tab-pipeline", data: { target: '#js-tab-pipeline', action: 'pipeline', toggle: 'tab' }, class: 'pipeline-tab'
+ %li
+ = link_to "#js-tab-builds", data: { target: '#js-tab-builds', action: 'build', toggle: 'tab' }, class: 'builds-tab' do
+ Builds
+ %span.badge= pipeline.statuses.count
+
+.tab-content
+ #js-tab-pipeline.tab-pane.active
+ .build-content.middle-block.pipeline-graph
+ .pipeline-visualization
+ %ul.stage-column-list
+ - stages = pipeline.stages_with_latest_statuses
+ - stages.each do |stage, statuses|
+ %li.stage-column
+ .stage-name
+ %a{name: stage}
+ - if stage
+ = stage.titleize
+ .builds-container
+ %ul
+ = render "projects/commit/pipeline_stage", statuses: statuses
+
+ #js-tab-builds.tab-pane
+ - if pipeline.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - pipeline.yaml_errors.split(",").each do |error|
+ %li= error
+ You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
+
+ - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
+ .bs-callout.bs-callout-warning
+ \.gitlab-ci.yml not found in this commit
+
+ .table-holder.pipeline-holder
+ %table.table.ci-table.pipeline
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Name
+ %th
+ - if pipeline.project.build_coverage_enabled?
+ %th Coverage
+ %th
+ - pipeline.statuses.relevant.stages.each do |stage|
+ = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 688535ad764..8c6652a5f90 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -3,9 +3,7 @@
= render "projects/pipelines/head"
%div{ class: container_class }
- .prepend-top-default
- - if @commit
- = render "projects/pipelines/info"
- %div.block-connector
+ - if @commit
+ = render "projects/pipelines/info"
- = render "projects/commit/pipeline", pipeline: @pipeline
+ = render "projects/pipelines/with_tabs", pipeline: @pipeline
diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg
index 3420af411f6..e0a2d4282f0 100644
--- a/app/views/shared/icons/_icon_status_skipped.svg
+++ b/app/views/shared/icons/_icon_status_skipped.svg
@@ -1 +1 @@
-<svg width="14" height="14" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
+<svg width="14" height="14" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index e0ad5268664..e17add7421f 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -4,15 +4,13 @@ class BuildSuccessWorker
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
- create_deployment(build)
+ create_deployment(build) if build.has_environment?
end
end
private
def create_deployment(build)
- return if build.environment.blank?
-
service = CreateDeploymentService.new(
build.project, build.user,
environment: build.environment,
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index 34f6ef161fb..070943f1ecc 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -12,11 +12,11 @@ class PipelineMetricsWorker
private
def update_metrics_for_active_pipeline(pipeline)
- metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
+ metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil, pipeline_id: pipeline.id)
end
def update_metrics_for_succeeded_pipeline(pipeline)
- metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at)
+ metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at, pipeline_id: pipeline.id)
end
def metrics(pipeline)
diff --git a/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml b/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml
new file mode 100644
index 00000000000..18fb8a6ad45
--- /dev/null
+++ b/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml
@@ -0,0 +1,4 @@
+---
+title: Make New Group form respect default visibility application setting
+merge_request: 7454
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/23223-group-deletion-race-condition.yml b/changelogs/unreleased/23223-group-deletion-race-condition.yml
new file mode 100644
index 00000000000..6f22e85fb4b
--- /dev/null
+++ b/changelogs/unreleased/23223-group-deletion-race-condition.yml
@@ -0,0 +1,4 @@
+---
+title: Fix race condition during group deletion and remove stale records present due to this bug
+merge_request: 7528
+author: Timothy Andrew
diff --git a/changelogs/unreleased/23637-title-bar-pipelines.yml b/changelogs/unreleased/23637-title-bar-pipelines.yml
new file mode 100644
index 00000000000..3d4cf88c54c
--- /dev/null
+++ b/changelogs/unreleased/23637-title-bar-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: Redesign pipelines page
+merge_request:
+author:
diff --git a/changelogs/unreleased/bugfix-html-only-mail.yml b/changelogs/unreleased/bugfix-html-only-mail.yml
new file mode 100644
index 00000000000..ea0d4e7396f
--- /dev/null
+++ b/changelogs/unreleased/bugfix-html-only-mail.yml
@@ -0,0 +1,4 @@
+---
+title: Add support for reply-by-email when the email only contains HTML
+merge_request: 7397
+author:
diff --git a/changelogs/unreleased/changelog-update.yml b/changelogs/unreleased/changelog-update.yml
new file mode 100644
index 00000000000..24fa34f4121
--- /dev/null
+++ b/changelogs/unreleased/changelog-update.yml
@@ -0,0 +1,4 @@
+---
+title: Add environment info to builds page
+merge_request:
+author:
diff --git a/changelogs/unreleased/feature-cycle-analytics-events.yml b/changelogs/unreleased/feature-cycle-analytics-events.yml
new file mode 100644
index 00000000000..e1211a3c774
--- /dev/null
+++ b/changelogs/unreleased/feature-cycle-analytics-events.yml
@@ -0,0 +1,4 @@
+---
+title: Add events per stage to cycle analytics
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-admin-ci-table.yml b/changelogs/unreleased/fix-admin-ci-table.yml
new file mode 100644
index 00000000000..9a9e39ee94a
--- /dev/null
+++ b/changelogs/unreleased/fix-admin-ci-table.yml
@@ -0,0 +1,4 @@
+---
+title: Fix misaligned buttons on admin builds page
+merge_request: 7424
+author: Didem Acet
diff --git a/changelogs/unreleased/issue_13232.yml b/changelogs/unreleased/issue_13232.yml
new file mode 100644
index 00000000000..6dc2de5afe4
--- /dev/null
+++ b/changelogs/unreleased/issue_13232.yml
@@ -0,0 +1,4 @@
+---
+title: Add JIRA remotelinks and prevent duplicated closing messages
+merge_request:
+author:
diff --git a/changelogs/unreleased/optimize-mr-index.yml b/changelogs/unreleased/optimize-mr-index.yml
new file mode 100644
index 00000000000..1090b6d4528
--- /dev/null
+++ b/changelogs/unreleased/optimize-mr-index.yml
@@ -0,0 +1,4 @@
+---
+title: More aggressively preload on merge request and issue index pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml b/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml
new file mode 100644
index 00000000000..5e868027ed6
--- /dev/null
+++ b/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml
@@ -0,0 +1,4 @@
+---
+title: Pass correct tag target to post-receive hook when creating tag via UI
+merge_request: 7556
+author:
diff --git a/config/no_todos_messages.yml b/config/no_todos_messages.yml
index 8372fb4ebe9..264a975b614 100644
--- a/config/no_todos_messages.yml
+++ b/config/no_todos_messages.yml
@@ -1,13 +1,11 @@
-# When the Todos list on the user's dashboard becomes empty, one of the messages below shows up randomly.
+# When the todo list on the user's dashboard becomes empty, a random message
+# from the list below will be shown.
#
# If you come up with a fun one, please feel free to contribute it to GitLab!
# https://about.gitlab.com/contributing/
-
---
- Good job! Looks like you don't have any todos left.
-- Coffee really tastes better without any todos left.
-- Isn't an empty To Do list beautiful?
-- Time for a rewarding coffee break
+- Isn't an empty todo list beautiful?
- Give yourself a pat on the back!
-- High five!
-- Hence forth you shall be known as 'Todo Destroyer' \ No newline at end of file
+- Nothing left to do, high five!
+- Henceforth you shall be known as "Todo Destroyer".
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 9cf8465dca8..d6eae1c9fce 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -153,6 +153,18 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
resource :cycle_analytics, only: [:show]
+ namespace :cycle_analytics do
+ scope :events, controller: 'events' do
+ get :issue
+ get :plan
+ get :code
+ get :test
+ get :review
+ get :staging
+ get :production
+ end
+ end
+
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index e882a492757..916ee8dbac8 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -203,6 +203,8 @@ class Gitlab::Seeder::CycleAnalytics
pipeline.run!
Timecop.travel rand(1..6).hours.from_now
pipeline.succeed!
+
+ PipelineMetricsWorker.new.perform(pipeline.id)
end
end
diff --git a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
new file mode 100644
index 00000000000..f49df6802a7
--- /dev/null
+++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
@@ -0,0 +1,33 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # 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 = 'Adding a foreign key'
+
+ # 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 :merge_request_metrics, :pipeline_id, :integer
+ add_concurrent_index :merge_request_metrics, :pipeline_id
+ add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade
+ end
+end
diff --git a/db/migrate/20161117114805_remove_undeleted_groups.rb b/db/migrate/20161117114805_remove_undeleted_groups.rb
new file mode 100644
index 00000000000..ebc2d974ae0
--- /dev/null
+++ b/db/migrate/20161117114805_remove_undeleted_groups.rb
@@ -0,0 +1,16 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUndeletedGroups < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute "DELETE FROM namespaces WHERE deleted_at IS NOT NULL;"
+ end
+
+ def down
+ # This is an irreversible migration;
+ # If someone is trying to rollback for other reasons, we should not throw an Exception.
+ # raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8f8a03e1534..19bd6b63acb 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20161113184239) do
+ActiveRecord::Schema.define(version: 20161117114805) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -649,10 +649,12 @@ ActiveRecord::Schema.define(version: 20161113184239) do
t.datetime "merged_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.integer "pipeline_id"
end
add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
add_index "merge_request_metrics", ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
+ add_index "merge_request_metrics", ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id", using: :btree
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
@@ -1260,6 +1262,7 @@ ActiveRecord::Schema.define(version: 20161113184239) do
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
+ add_foreign_key "merge_request_metrics", "ci_commits", column: "pipeline_id", on_delete: :cascade
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
diff --git a/doc/api/projects.md b/doc/api/projects.md
index bbb3bfb4995..467a880ac13 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -850,7 +850,7 @@ POST /projects/:id/archive
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/archive"
```
Example response:
@@ -939,7 +939,7 @@ POST /projects/:id/unarchive
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unarchive"
```
Example response:
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
index 7412fdbbc78..85f8849fa99 100644
--- a/doc/ci/examples/test-scala-application.md
+++ b/doc/ci/examples/test-scala-application.md
@@ -1,11 +1,11 @@
-## Test a Scala application
+# Test and deploy to Heroku a Scala application
This example demonstrates the integration of Gitlab CI with Scala
applications using SBT. Checkout the example
[project](https://gitlab.com/gitlab-examples/scala-sbt) and
[build status](https://gitlab.com/gitlab-examples/scala-sbt/builds).
-### Add `.gitlab-ci.yml` file to project
+## Add `.gitlab-ci.yml` file to project
The following `.gitlab-ci.yml` should be added in the root of your
repository to trigger CI:
@@ -13,10 +13,14 @@ repository to trigger CI:
``` yaml
image: java:8
+stages:
+ - test
+ - deploy
+
before_script:
- apt-get update -y
- apt-get install apt-transport-https -y
- # Install SBT
+ ## Install SBT
- echo "deb http://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list
- apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 642AC823
- apt-get update -y
@@ -24,8 +28,17 @@ before_script:
- sbt sbt-version
test:
+ stage: test
script:
- sbt clean coverage test coverageReport
+
+deploy:
+ stage: deploy
+ script:
+ - apt-get update -yq
+ - apt-get install rubygems ruby-dev -y
+ - gem install dpl
+ - dpl --provider=heroku --app=gitlab-play-sample-app --api-key=$HEROKU_API_KEY
```
The `before_script` installs [SBT](http://www.scala-sbt.org/) and
@@ -33,15 +46,31 @@ displays the version that is being used. The `test` stage executes SBT
to compile and test the project.
[scoverage](https://github.com/scoverage/sbt-scoverage) is used as an SBT
plugin to measure test coverage.
+The `deploy` stage automatically deploys the project to Heroku using dpl.
You can use other versions of Scala and SBT by defining them in
`build.sbt`.
-### Display test coverage in build
+## Display test coverage in build
Add the `Coverage was \[\d+.\d+\%\]` regular expression in the
-**Settings > Edit Project > Test coverage parsing** project setting to
-retrieve the test coverage rate from the build trace and have it
+**Settings âž” Edit Project âž” Test coverage parsing** project setting to
+retrieve the [test coverage] rate from the build trace and have it
displayed with your builds.
**Builds** must be enabled for this option to appear.
+
+## Heroku application
+
+A Heroku application is required. You can create one through the
+[Dashboard](https://dashboard.heroku.com/). Substitute `gitlab-play-sample-app`
+in the `.gitlab-ci.yml` file with your application's name.
+
+## Heroku API key
+
+You can look up your Heroku API key in your
+[account](https://dashboard.heroku.com/account). Add a secure [variable] with
+this value in **Project âž” Variables** with key `HEROKU_API_KEY`.
+
+[variable]: ../variables/README.md#user-defined-variables-secure-variables
+[test coverage]: ../../user/project/pipelines/settings.md#test-coverage-report-badge
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 6fee750c709..338c9a27789 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -760,6 +760,15 @@ artifacts:
- binaries/
```
+To disable artifact passing, define the job with empty [dependencies](#dependencies):
+
+```yaml
+job:
+ stage: build
+ script: make build
+ dependencies: []
+```
+
You may want to create artifacts only for tagged releases to avoid filling the
build server storage with temporary build artifacts.
diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb
new file mode 100644
index 00000000000..486139b1687
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_event.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module CycleAnalytics
+ class BaseEvent
+ include MetricsTables
+
+ 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
+ end
+
+ def fetch
+ update_author!
+
+ event_result.map do |event|
+ serialize(event) if has_permission?(event['id'])
+ end
+ 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 ||= @query.execute(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/code_event.rb b/lib/gitlab/cycle_analytics/code_event.rb
new file mode 100644
index 00000000000..2afdf0b8518
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/code_event.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module CycleAnalytics
+ class CodeEvent < BaseEvent
+ 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],
+ 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/events.rb b/lib/gitlab/cycle_analytics/events.rb
new file mode 100644
index 00000000000..2d703d76cbb
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/events.rb
@@ -0,0 +1,38 @@
+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
new file mode 100644
index 00000000000..2418832ccc2
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/events_query.rb
@@ -0,0 +1,37 @@
+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_allowed.rb b/lib/gitlab/cycle_analytics/issue_allowed.rb
new file mode 100644
index 00000000000..a7652a70641
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/issue_allowed.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module IssueAllowed
+ def allowed_ids
+ @allowed_ids ||= IssuesFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb
new file mode 100644
index 00000000000..705b7e5ce24
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/issue_event.rb
@@ -0,0 +1,27 @@
+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/merge_request_allowed.rb b/lib/gitlab/cycle_analytics/merge_request_allowed.rb
new file mode 100644
index 00000000000..28f6db44759
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/merge_request_allowed.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module MergeRequestAllowed
+ def allowed_ids
+ @allowed_ids ||= MergeRequestsFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb
new file mode 100644
index 00000000000..b71e8735e27
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/metrics_fetcher.rb
@@ -0,0 +1,60 @@
+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 <issue,merge_request> 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/metrics_tables.rb b/lib/gitlab/cycle_analytics/metrics_tables.rb
new file mode 100644
index 00000000000..9d25ef078e8
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/metrics_tables.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module CycleAnalytics
+ module MetricsTables
+ def mr_metrics_table
+ MergeRequest::Metrics.arel_table
+ end
+
+ def mr_table
+ MergeRequest.arel_table
+ end
+
+ def mr_diff_table
+ MergeRequestDiff.arel_table
+ end
+
+ def mr_closing_issues_table
+ MergeRequestsClosingIssues.arel_table
+ end
+
+ def issue_table
+ Issue.arel_table
+ end
+
+ def issue_metrics_table
+ Issue::Metrics.arel_table
+ end
+
+ def user_table
+ User.arel_table
+ end
+
+ def build_table
+ ::CommitStatus.arel_table
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event.rb
new file mode 100644
index 00000000000..b1ae215f348
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/plan_event.rb
@@ -0,0 +1,44 @@
+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]]
+ @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)
+ 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/production_event.rb b/lib/gitlab/cycle_analytics/production_event.rb
new file mode 100644
index 00000000000..4868c3c6237
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_event.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module CycleAnalytics
+ class ProductionEvent < BaseEvent
+ 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],
+ 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/review_event.rb b/lib/gitlab/cycle_analytics/review_event.rb
new file mode 100644
index 00000000000..b394a02cc52
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/review_event.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module CycleAnalytics
+ class ReviewEvent < BaseEvent
+ 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]]
+
+ 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/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event.rb
new file mode 100644
index 00000000000..a1f30b716f6
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/staging_event.rb
@@ -0,0 +1,31 @@
+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]]
+ @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/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb
new file mode 100644
index 00000000000..d553d0b5aec
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/test_event.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module CycleAnalytics
+ class TestEvent < StagingEvent
+ 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
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/updater.rb b/lib/gitlab/cycle_analytics/updater.rb
new file mode 100644
index 00000000000..953268ebd46
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/updater.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module CycleAnalytics
+ class Updater
+ def self.update!(*args)
+ new(*args).update!
+ end
+
+ def initialize(event_result, from:, to:, klass:)
+ @event_result = event_result
+ @klass = klass
+ @from = from
+ @to = to
+ end
+
+ def update!
+ @event_result.each do |event|
+ event[@to] = items[event.delete(@from).to_i].first
+ end
+ end
+
+ def result_ids
+ @event_result.map { |event| event[@from] }
+ end
+
+ def items
+ @items ||= @klass.find(result_ids).group_by { |item| item['id'] }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/date_time.rb b/lib/gitlab/database/date_time.rb
index b6a89f715fd..25e56998038 100644
--- a/lib/gitlab/database/date_time.rb
+++ b/lib/gitlab/database/date_time.rb
@@ -7,21 +7,25 @@ module Gitlab
#
# Note: For MySQL, the interval is returned in seconds.
# For PostgreSQL, the interval is returned as an INTERVAL type.
- def subtract_datetimes(query_so_far, end_time_attrs, start_time_attrs, as)
- diff_fn = if Gitlab::Database.postgresql?
- Arel::Nodes::Subtraction.new(
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs)),
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)))
- elsif Gitlab::Database.mysql?
- Arel::Nodes::NamedFunction.new(
- "TIMESTAMPDIFF",
- [Arel.sql('second'),
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)),
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs))])
- end
+ def subtract_datetimes(query_so_far, start_time_attrs, end_time_attrs, as)
+ diff_fn = subtract_datetimes_diff(query_so_far, start_time_attrs, end_time_attrs)
query_so_far.project(diff_fn.as(as))
end
+
+ def subtract_datetimes_diff(query_so_far, start_time_attrs, end_time_attrs)
+ if Gitlab::Database.postgresql?
+ Arel::Nodes::Subtraction.new(
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)))
+ elsif Gitlab::Database.mysql?
+ Arel::Nodes::NamedFunction.new(
+ "TIMESTAMPDIFF",
+ [Arel.sql('second'),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs))])
+ end
+ end
end
end
end
diff --git a/lib/gitlab/email/html_parser.rb b/lib/gitlab/email/html_parser.rb
new file mode 100644
index 00000000000..a4ca62bfc41
--- /dev/null
+++ b/lib/gitlab/email/html_parser.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module Email
+ class HTMLParser
+ def self.parse_reply(raw_body)
+ new(raw_body).filtered_text
+ end
+
+ attr_reader :raw_body
+ def initialize(raw_body)
+ @raw_body = raw_body
+ end
+
+ def document
+ @document ||= Nokogiri::HTML.parse(raw_body)
+ end
+
+ def filter_replies!
+ document.xpath('//blockquote').each(&:remove)
+ document.xpath('//table').each(&:remove)
+ end
+
+ def filtered_html
+ @filtered_html ||= begin
+ filter_replies!
+ document.inner_html
+ end
+ end
+
+ def filtered_text
+ @filtered_text ||= Html2Text.convert(filtered_html)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 3411eb1d9ce..85402c2a278 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -23,19 +23,26 @@ module Gitlab
private
def select_body(message)
- text = message.text_part if message.multipart?
- text ||= message if message.content_type !~ /text\/html/
+ if message.multipart?
+ part = message.text_part || message.html_part || message
+ else
+ part = message
+ end
- return "" unless text
+ decoded = fix_charset(part)
- text = fix_charset(text)
+ return "" unless decoded
# Certain trigger phrases that means we didn't parse correctly
- if text =~ /(Content\-Type\:|multipart\/alternative|text\/plain)/
+ if decoded =~ /(Content\-Type\:|multipart\/alternative|text\/plain)/
return ""
end
- text
+ if (part.content_type || '').include? 'text/html'
+ HTMLParser.parse_reply(decoded)
+ else
+ decoded
+ end
end
# Force encoding to UTF-8 on a Mail::Message or Mail::Part
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
new file mode 100644
index 00000000000..f6d625fa7f6
--- /dev/null
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+feature 'Admin Groups', feature: true do
+ let(:internal) { Gitlab::VisibilityLevel::INTERNAL }
+
+ before do
+ login_as(:admin)
+
+ stub_application_setting(default_group_visibility: internal)
+ end
+
+ describe 'create a group' do
+ scenario 'shows the visibility level radio populated with the default value' do
+ visit new_admin_group_path
+
+ expect_selected_visibility(internal)
+ end
+ end
+
+ describe 'group edit' do
+ scenario 'shows the visibility level radio populated with the group visibility_level value' do
+ group = create(:group, :private)
+
+ visit edit_admin_group_path(group)
+
+ expect_selected_visibility(group.visibility_level)
+ end
+ end
+
+ def expect_selected_visibility(level)
+ selector = "#group_visibility_level_#{level}[checked=checked]"
+
+ expect(page).to have_selector(selector, count: 1)
+ end
+end
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index e45e3a36d01..d46d9e9399e 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -64,7 +64,7 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit from a different branch", js: true do
it do
- find('.commit-action-buttons a.dropdown-toggle').click
+ find('.header-action-buttons a.dropdown-toggle').click
find(:css, "a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
index db56a50e058..002c6f6b359 100644
--- a/spec/features/projects/pipelines_spec.rb
+++ b/spec/features/projects/pipelines_spec.rb
@@ -146,7 +146,8 @@ describe "Pipelines" do
end
describe 'GET /:project/pipelines/:id' do
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
before do
@success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
diff --git a/spec/fixtures/emails/outlook_html.eml b/spec/fixtures/emails/outlook_html.eml
new file mode 100644
index 00000000000..506d69efe83
--- /dev/null
+++ b/spec/fixtures/emails/outlook_html.eml
@@ -0,0 +1,140 @@
+
+MIME-Version: 1.0
+Received: by 10.25.161.144 with HTTP; Tue, 7 Oct 2014 22:17:17 -0700 (PDT)
+X-Originating-IP: [117.207.85.84]
+In-Reply-To: <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail>
+References: <topic/35@discourse.techapj.com>
+ <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail>
+Date: Wed, 8 Oct 2014 10:47:17 +0530
+Delivered-To: arpit@techapj.com
+Message-ID: <CAOJeqne=SJ_LwN4sb-0Y95ejc2OpreVhdmcPn0TnmwSvTCYzzQ@mail.gmail.com>
+Subject: Re: [Discourse] [Meta] Welcome to techAPJ's Discourse!
+From: Arpit Jalan <arpit@techapj.com>
+To: Discourse <mail+e1c7f2a380e33840aeb654f075490bad@arpitjalan.com>Accept-Language: en-US
+Content-Language: en-US
+X-MS-Has-Attach:
+X-MS-TNEF-Correlator:
+x-originating-ip: [134.68.31.227]
+Content-Type: multipart/alternative;
+ boundary="_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_"
+MIME-Version: 1.0
+
+--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: base64
+
+PGh0bWwgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIHhtbG5zOm89InVy
+bjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSIgeG1sbnM6dz0idXJuOnNjaGVt
+YXMtbWljcm9zb2Z0LWNvbTpvZmZpY2U6d29yZCIgeG1sbnM6bT0iaHR0cDovL3NjaGVtYXMubWlj
+cm9zb2Z0LmNvbS9vZmZpY2UvMjAwNC8xMi9vbW1sIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv
+VFIvUkVDLWh0bWw0MCI+DQo8aGVhZD4NCjxtZXRhIGh0dHAtZXF1aXY9IkNvbnRlbnQtVHlwZSIg
+Y29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04Ij4NCjxtZXRhIG5hbWU9IkdlbmVyYXRv
+ciIgY29udGVudD0iTWljcm9zb2Z0IFdvcmQgMTQgKGZpbHRlcmVkIG1lZGl1bSkiPg0KPCEtLVtp
+ZiAhbXNvXT48c3R5bGU+dlw6KiB7YmVoYXZpb3I6dXJsKCNkZWZhdWx0I1ZNTCk7fQ0Kb1w6KiB7
+YmVoYXZpb3I6dXJsKCNkZWZhdWx0I1ZNTCk7fQ0Kd1w6KiB7YmVoYXZpb3I6dXJsKCNkZWZhdWx0
+I1ZNTCk7fQ0KLnNoYXBlIHtiZWhhdmlvcjp1cmwoI2RlZmF1bHQjVk1MKTt9DQo8L3N0eWxlPjwh
+W2VuZGlmXS0tPjxzdHlsZT48IS0tDQovKiBGb250IERlZmluaXRpb25zICovDQpAZm9udC1mYWNl
+DQoJe2ZvbnQtZmFtaWx5OkNhbGlicmk7DQoJcGFub3NlLTE6MiAxNSA1IDIgMiAyIDQgMyAyIDQ7
+fQ0KQGZvbnQtZmFjZQ0KCXtmb250LWZhbWlseTpUYWhvbWE7DQoJcGFub3NlLTE6MiAxMSA2IDQg
+MyA1IDQgNCAyIDQ7fQ0KLyogU3R5bGUgRGVmaW5pdGlvbnMgKi8NCnAuTXNvTm9ybWFsLCBsaS5N
+c29Ob3JtYWwsIGRpdi5Nc29Ob3JtYWwNCgl7bWFyZ2luOjBpbjsNCgltYXJnaW4tYm90dG9tOi4w
+MDAxcHQ7DQoJZm9udC1zaXplOjEyLjBwdDsNCglmb250LWZhbWlseToiVGltZXMgTmV3IFJvbWFu
+Iiwic2VyaWYiO30NCmE6bGluaywgc3Bhbi5Nc29IeXBlcmxpbmsNCgl7bXNvLXN0eWxlLXByaW9y
+aXR5Ojk5Ow0KCWNvbG9yOmJsdWU7DQoJdGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZTt9DQphOnZp
+c2l0ZWQsIHNwYW4uTXNvSHlwZXJsaW5rRm9sbG93ZWQNCgl7bXNvLXN0eWxlLXByaW9yaXR5Ojk5
+Ow0KCWNvbG9yOnB1cnBsZTsNCgl0ZXh0LWRlY29yYXRpb246dW5kZXJsaW5lO30NCnANCgl7bXNv
+LXN0eWxlLXByaW9yaXR5Ojk5Ow0KCW1zby1tYXJnaW4tdG9wLWFsdDphdXRvOw0KCW1hcmdpbi1y
+aWdodDowaW47DQoJbXNvLW1hcmdpbi1ib3R0b20tYWx0OmF1dG87DQoJbWFyZ2luLWxlZnQ6MGlu
+Ow0KCWZvbnQtc2l6ZToxMi4wcHQ7DQoJZm9udC1mYW1pbHk6IlRpbWVzIE5ldyBSb21hbiIsInNl
+cmlmIjt9DQpzcGFuLkVtYWlsU3R5bGUxOA0KCXttc28tc3R5bGUtdHlwZTpwZXJzb25hbC1yZXBs
+eTsNCglmb250LWZhbWlseToiQ2FsaWJyaSIsInNhbnMtc2VyaWYiOw0KCWNvbG9yOiMxRjQ5N0Q7
+fQ0KLk1zb0NocERlZmF1bHQNCgl7bXNvLXN0eWxlLXR5cGU6ZXhwb3J0LW9ubHk7DQoJZm9udC1m
+YW1pbHk6IkNhbGlicmkiLCJzYW5zLXNlcmlmIjt9DQpAcGFnZSBXb3JkU2VjdGlvbjENCgl7c2l6
+ZTo4LjVpbiAxMS4waW47DQoJbWFyZ2luOjEuMGluIDEuMGluIDEuMGluIDEuMGluO30NCmRpdi5X
+b3JkU2VjdGlvbjENCgl7cGFnZTpXb3JkU2VjdGlvbjE7fQ0KLS0+PC9zdHlsZT48IS0tW2lmIGd0
+ZSBtc28gOV0+PHhtbD4NCjxvOnNoYXBlZGVmYXVsdHMgdjpleHQ9ImVkaXQiIHNwaWRtYXg9IjEw
+MjYiIC8+DQo8L3htbD48IVtlbmRpZl0tLT48IS0tW2lmIGd0ZSBtc28gOV0+PHhtbD4NCjxvOnNo
+YXBlbGF5b3V0IHY6ZXh0PSJlZGl0Ij4NCjxvOmlkbWFwIHY6ZXh0PSJlZGl0IiBkYXRhPSIxIiAv
+Pg0KPC9vOnNoYXBlbGF5b3V0PjwveG1sPjwhW2VuZGlmXS0tPg0KPC9oZWFkPg0KPGJvZHkgbGFu
+Zz0iRU4tVVMiIGxpbms9ImJsdWUiIHZsaW5rPSJwdXJwbGUiPg0KPGRpdiBjbGFzcz0iV29yZFNl
+Y3Rpb24xIj4NCjxwIGNsYXNzPSJNc29Ob3JtYWwiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTEu
+MHB0O2ZvbnQtZmFtaWx5OiZxdW90O0NhbGlicmkmcXVvdDssJnF1b3Q7c2Fucy1zZXJpZiZxdW90
+Oztjb2xvcjojMUY0OTdEIj5NaWNyb3NvZnQgT3V0bG9vayAyMDEwPG86cD48L286cD48L3NwYW4+
+PC9wPg0KPHAgY2xhc3M9Ik1zb05vcm1hbCI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMS4wcHQ7
+Zm9udC1mYW1pbHk6JnF1b3Q7Q2FsaWJyaSZxdW90OywmcXVvdDtzYW5zLXNlcmlmJnF1b3Q7O2Nv
+bG9yOiMxRjQ5N0QiPjxvOnA+Jm5ic3A7PC9vOnA+PC9zcGFuPjwvcD4NCjxwIGNsYXNzPSJNc29O
+b3JtYWwiPjxiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTAuMHB0O2ZvbnQtZmFtaWx5OiZxdW90
+O1RhaG9tYSZxdW90OywmcXVvdDtzYW5zLXNlcmlmJnF1b3Q7Ij5Gcm9tOjwvc3Bhbj48L2I+PHNw
+YW4gc3R5bGU9ImZvbnQtc2l6ZToxMC4wcHQ7Zm9udC1mYW1pbHk6JnF1b3Q7VGFob21hJnF1b3Q7
+LCZxdW90O3NhbnMtc2VyaWYmcXVvdDsiPiBtaWNoYWVsIFttYWlsdG86dGFsa0BvcGVubXJzLm9y
+Z10NCjxicj4NCjxiPlNlbnQ6PC9iPiBNb25kYXksIE9jdG9iZXIgMTMsIDIwMTQgOTozOCBBTTxi
+cj4NCjxiPlRvOjwvYj4gUG93ZXIsIENocmlzPGJyPg0KPGI+U3ViamVjdDo8L2I+IFtQTV0gWW91
+ciBwb3N0IGluICZxdW90O0J1cmdlcmhhdXM6IE5ldyByZXN0YXVyYW50IC8gbHVuY2ggdmVudWUm
+cXVvdDs8bzpwPjwvbzpwPjwvc3Bhbj48L3A+DQo8cCBjbGFzcz0iTXNvTm9ybWFsIj48bzpwPiZu
+YnNwOzwvbzpwPjwvcD4NCjxkaXY+DQo8dGFibGUgY2xhc3M9Ik1zb05vcm1hbFRhYmxlIiBib3Jk
+ZXI9IjAiIGNlbGxzcGFjaW5nPSIwIiBjZWxscGFkZGluZz0iMCI+DQo8dGJvZHk+DQo8dHI+DQo8
+dGQgdmFsaWduPSJ0b3AiIHN0eWxlPSJwYWRkaW5nOjBpbiAwaW4gMGluIDBpbiI+PC90ZD4NCjx0
+ZCBzdHlsZT0icGFkZGluZzowaW4gMGluIDBpbiAwaW4iPg0KPHAgY2xhc3M9Ik1zb05vcm1hbCIg
+c3R5bGU9Im1hcmdpbi1ib3R0b206MTguNzVwdCI+PGEgaHJlZj0iaHR0cDovL2NsLm9wZW5tcnMu
+b3JnL3RyYWNrL2NsaWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5vcmc/cD1leUp6SWpvaWJHbFph
+MVYwZVhoQ1kwMU1SVEZzVURKbVl6VlFNMFpsZWpFNElpd2lkaUk2TVN3aWNDSTZJbnRjSW5WY0lq
+b3pNREF6T1Rrd05TeGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9kSFJ3Y3pwY1hGd3ZYRnhjTDNS
+aGJHc3ViM0JsYm0xeWN5NXZjbWRjWEZ3dmRYTmxjbk5jWEZ3dmJXbGphR0ZsYkZ3aUxGd2lhV1Jj
+SWpwY0ltUTFZbU13TjJOa05EUmpaRFE0TUdNNFlUZzJNemxqWldJMU56Z3pZbVkyWENJc1hDSjFj
+bXhmYVdSelhDSTZXMXdpWWpoa09EZzFNams1TnpkbVpqWTFaV1l5TlRFM09XUmlOR1l5TVdJM056
+RmpOemhqWmpoa09Gd2lYWDBpZlEiIHRhcmdldD0iX2JsYW5rIj48Yj48c3BhbiBzdHlsZT0iZm9u
+dC1zaXplOjEwLjBwdDtmb250LWZhbWlseTomcXVvdDtUYWhvbWEmcXVvdDssJnF1b3Q7c2Fucy1z
+ZXJpZiZxdW90Oztjb2xvcjojMDA2Njk5O3RleHQtZGVjb3JhdGlvbjpub25lIj5taWNoYWVsPC9z
+cGFuPjwvYj48L2E+PGJyPg0KPHNwYW4gc3R5bGU9ImZvbnQtc2l6ZTo4LjVwdDtmb250LWZhbWls
+eTomcXVvdDtUYWhvbWEmcXVvdDssJnF1b3Q7c2Fucy1zZXJpZiZxdW90Oztjb2xvcjojOTk5OTk5
+Ij5PY3RvYmVyIDEzPC9zcGFuPg0KPG86cD48L286cD48L3A+DQo8L3RkPg0KPC90cj4NCjx0cj4N
+Cjx0ZCBjb2xzcGFuPSIyIiBzdHlsZT0icGFkZGluZzozLjc1cHQgMGluIDBpbiAwaW4iPg0KPHAg
+Y2xhc3M9Ik1zb05vcm1hbCIgc3R5bGU9Im1hcmdpbi1ib3R0b206MTguNzVwdCI+PGEgaHJlZj0i
+aHR0cDovL2NsLm9wZW5tcnMub3JnL3RyYWNrL2NsaWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5v
+cmc/cD1leUp6SWpvaVVFUklTVU55UjNsVk1EZEJWVmhwV25SM1dXeDRNV05zVFc1Wklpd2lkaUk2
+TVN3aWNDSTZJbnRjSW5WY0lqb3pNREF6T1Rrd05TeGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9k
+SFJ3Y3pwY1hGd3ZYRnhjTDNSaGJHc3ViM0JsYm0xeWN5NXZjbWRjWEZ3dmRGeGNYQzlpZFhKblpY
+Sm9ZWFZ6TFc1bGR5MXlaWE4wWVhWeVlXNTBMV3gxYm1Ob0xYWmxiblZsWEZ4Y0x6WTNNbHhjWEM4
+elhDSXNYQ0pwWkZ3aU9sd2laRFZpWXpBM1kyUTBOR05rTkRnd1l6aGhPRFl6T1dObFlqVTNPRE5p
+WmpaY0lpeGNJblZ5YkY5cFpITmNJanBiWENKaU56WmlZamswWlRGaU56STVaVGsyWlRSbFpXTTRO
+R1JtTWpRNE1ETXdZall5WVdZeU1HTTBYQ0pkZlNKOSI+PGI+PHNwYW4gc3R5bGU9ImNvbG9yOiMw
+MDY2OTk7dGV4dC1kZWNvcmF0aW9uOm5vbmUiPmh0dHBzOi8vdGFsay5vcGVubXJzLm9yZy90L2J1
+cmdlcmhhdXMtbmV3LXJlc3RhdXJhbnQtbHVuY2gtdmVudWUvNjcyLzM8L3NwYW4+PC9iPjwvYT4N
+CjxvOnA+PC9vOnA+PC9wPg0KPHAgc3R5bGU9Im1hcmdpbi10b3A6MGluIj5Mb29rcyBsaWtlIHlv
+dXIgcmVwbHktYnktZW1haWwgd2Fzbid0IHByb2Nlc3NlZCBjb3JyZWN0bHkgYnkgb3VyIHNvZnR3
+YXJlLiBDYW4geW91IGxldCBtZSBrbm93IHdoYXQgdmVyc2lvbi9PUyBvZiB3aGF0IGVtYWlsIHBy
+b2dyYW0geW91J3JlIHVzaW5nPyBXZSB3aWxsIHdhbnQgdG8gdHJ5IHRvIGZpeCB0aGUgYnVnLiA6
+c21pbGU6PG86cD48L286cD48L3A+DQo8cCBzdHlsZT0ibWFyZ2luLXRvcDowaW4iPlRoYW5rcyE8
+bzpwPjwvbzpwPjwvcD4NCjwvdGQ+DQo8L3RyPg0KPC90Ym9keT4NCjwvdGFibGU+DQo8ZGl2IGNs
+YXNzPSJNc29Ob3JtYWwiIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJ0ZXh0LWFsaWduOmNlbnRlciI+
+DQo8aHIgc2l6ZT0iMSIgd2lkdGg9IjEwMCUiIGFsaWduPSJjZW50ZXIiPg0KPC9kaXY+DQo8ZGl2
+Pg0KPHA+PHNwYW4gc3R5bGU9ImNvbG9yOiM2NjY2NjYiPlRvIHJlc3BvbmQsIHJlcGx5IHRvIHRo
+aXMgZW1haWwgb3IgdmlzaXQgPGEgaHJlZj0iaHR0cDovL2NsLm9wZW5tcnMub3JnL3RyYWNrL2Ns
+aWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5vcmc/cD1leUp6SWpvaWVYaDJWbnBGTUhSMU1uRm5a
+RWR1TlhFd01GcFFPVlp0VFZvNElpd2lkaUk2TVN3aWNDSTZJbnRjSW5WY0lqb3pNREF6T1Rrd05T
+eGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9kSFJ3Y3pwY1hGd3ZYRnhjTDNSaGJHc3ViM0JsYm0x
+eWN5NXZjbWRjWEZ3dmRGeGNYQzk1YjNWeUxYQnZjM1F0YVc0dFluVnlaMlZ5YUdGMWN5MXVaWGN0
+Y21WemRHRjFjbUZ1ZEMxc2RXNWphQzEyWlc1MVpWeGNYQzgyTnpSY1hGd3ZNVndpTEZ3aWFXUmNJ
+anBjSW1RMVltTXdOMk5rTkRSalpEUTRNR000WVRnMk16bGpaV0kxTnpnelltWTJYQ0lzWENKMWNt
+eGZhV1J6WENJNlcxd2lZamMyWW1JNU5HVXhZamN5T1dVNU5tVTBaV1ZqT0RSa1pqSTBPREF6TUdJ
+Mk1tRm1NakJqTkZ3aVhYMGlmUSI+DQo8Yj48c3BhbiBzdHlsZT0iY29sb3I6IzAwNjY5OTt0ZXh0
+LWRlY29yYXRpb246bm9uZSI+aHR0cHM6Ly90YWxrLm9wZW5tcnMub3JnL3QveW91ci1wb3N0LWlu
+LWJ1cmdlcmhhdXMtbmV3LXJlc3RhdXJhbnQtbHVuY2gtdmVudWUvNjc0LzE8L3NwYW4+PC9iPjwv
+YT4gaW4geW91ciBicm93c2VyLjxvOnA+PC9vOnA+PC9zcGFuPjwvcD4NCjwvZGl2Pg0KPGRpdj4N
+CjxwPjxzcGFuIHN0eWxlPSJjb2xvcjojNjY2NjY2Ij5UbyB1bnN1YnNjcmliZSBmcm9tIHRoZXNl
+IGVtYWlscywgdmlzaXQgeW91ciA8YSBocmVmPSJodHRwOi8vY2wub3Blbm1ycy5vcmcvdHJhY2sv
+Y2xpY2svMzAwMzk5MDUvdGFsay5vcGVubXJzLm9yZz9wPWV5SnpJam9pZFV4dVdsZzVWRmMwT1da
+V1MwWTRiRmRMZG1seVdHc3hUVjl6SWl3aWRpSTZNU3dpY0NJNkludGNJblZjSWpvek1EQXpPVGt3
+TlN4Y0luWmNJam94TEZ3aWRYSnNYQ0k2WENKb2RIUndjenBjWEZ3dlhGeGNMM1JoYkdzdWIzQmxi
+bTF5Y3k1dmNtZGNYRnd2YlhsY1hGd3ZjSEpsWm1WeVpXNWpaWE5jSWl4Y0ltbGtYQ0k2WENKa05X
+SmpNRGRqWkRRMFkyUTBPREJqT0dFNE5qTTVZMlZpTlRjNE0ySm1ObHdpTEZ3aWRYSnNYMmxrYzF3
+aU9sdGNJbUk0TVdVd1pqQTFORFk1TkRNME56Z3lNMkZtTWpBMk5qRmpaamMzWkdOaU4yTmhZemRt
+TWpKY0lsMTlJbjAiPg0KPGI+PHNwYW4gc3R5bGU9ImNvbG9yOiMwMDY2OTk7dGV4dC1kZWNvcmF0
+aW9uOm5vbmUiPnVzZXIgcHJlZmVyZW5jZXM8L3NwYW4+PC9iPjwvYT4uPG86cD48L286cD48L3Nw
+YW4+PC9wPg0KPC9kaXY+DQo8L2Rpdj4NCjxwIGNsYXNzPSJNc29Ob3JtYWwiPjxpbWcgYm9yZGVy
+PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBpZD0iX3gwMDAwX2kxMDI2IiBzcmM9Imh0dHA6Ly9j
+bC5vcGVubXJzLm9yZy90cmFjay9vcGVuLnBocD91PTMwMDM5OTA1JmFtcDtpZD1kNWJjMDdjZDQ0
+Y2Q0ODBjOGE4NjM5Y2ViNTc4M2JmNiI+PG86cD48L286cD48L3A+DQo8L2Rpdj4NCjwvYm9keT4N
+CjwvaHRtbD4NCg==
+
+--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_--
diff --git a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb
new file mode 100644
index 00000000000..43f42d1bde8
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::CodeEvent do
+ 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/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
new file mode 100644
index 00000000000..9aeaa6b3ee8
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -0,0 +1,326 @@
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::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 }) }
+
+ before do
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context])
+
+ setup(context)
+ end
+
+ describe '#issue_events' do
+ it 'has the total time' do
+ expect(subject.issue_events.first[:total_time]).not_to be_empty
+ end
+
+ it 'has a title' do
+ expect(subject.issue_events.first[:title]).to eq(context.title)
+ end
+
+ it 'has the URL' do
+ expect(subject.issue_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)
+ end
+
+ it 'has a created_at timestamp' do
+ expect(subject.issue_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
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.issue_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)
+ end
+ end
+
+ describe '#plan_events' do
+ it 'has a title' do
+ expect(subject.plan_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
+ end
+
+ it 'has the URL' do
+ expect(subject.plan_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
+ end
+
+ it "has the author's URL" do
+ expect(subject.plan_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
+ end
+
+ it "has the author's name" do
+ expect(subject.plan_events.first[:author][:name]).not_to be_nil
+ end
+ end
+
+ describe '#code_events' do
+ 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
+ end
+
+ it 'has a title' do
+ expect(subject.code_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)
+ end
+
+ it 'has a created_at timestamp' do
+ expect(subject.code_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
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.code_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)
+ end
+ end
+
+ describe '#test_events' do
+ let(:merge_request) { MergeRequest.first }
+ let!(:pipeline) do
+ create(:ci_pipeline,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: context.project)
+ end
+
+ before do
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ pipeline.run!
+ pipeline.succeed!
+ end
+
+ it 'has the name' do
+ expect(subject.test_events.first[:name]).not_to be_nil
+ end
+
+ it 'has the ID' do
+ expect(subject.test_events.first[:id]).not_to be_nil
+ end
+
+ it 'has the URL' do
+ expect(subject.test_events.first[:url]).not_to be_nil
+ end
+
+ it 'has the branch name' do
+ expect(subject.test_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
+ end
+
+ it 'has the short SHA' do
+ expect(subject.test_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
+ end
+
+ it 'has the date' do
+ expect(subject.test_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
+ end
+ end
+
+ describe '#review_events' do
+ 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
+ end
+
+ it 'has a title' do
+ expect(subject.review_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)
+ end
+
+ it 'has the URL' do
+ expect(subject.review_events.first[:url]).not_to be_nil
+ end
+
+ it 'has a state' do
+ expect(subject.review_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
+ end
+
+ it "has the author's URL" do
+ expect(subject.review_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
+ end
+
+ it "has the author's name" do
+ expect(subject.review_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ end
+ end
+
+ describe '#staging_events' do
+ let(:merge_request) { MergeRequest.first }
+ let!(:pipeline) do
+ create(:ci_pipeline,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: context.project)
+ end
+
+ before do
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ pipeline.run!
+ pipeline.succeed!
+
+ merge_merge_requests_closing_issue(context)
+ deploy_master
+ end
+
+ it 'has the name' do
+ expect(subject.staging_events.first[:name]).not_to be_nil
+ end
+
+ it 'has the ID' do
+ expect(subject.staging_events.first[:id]).not_to be_nil
+ end
+
+ it 'has the URL' do
+ expect(subject.staging_events.first[:url]).not_to be_nil
+ end
+
+ it 'has the branch name' do
+ expect(subject.staging_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
+ end
+
+ it 'has the short SHA' do
+ expect(subject.staging_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
+ end
+
+ it 'has the date' do
+ expect(subject.staging_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
+ end
+
+ it "has the author's URL" do
+ expect(subject.staging_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
+ end
+
+ it "has the author's name" do
+ expect(subject.staging_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ end
+ end
+
+ describe '#production_events' do
+ let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
+
+ before do
+ merge_merge_requests_closing_issue(context)
+ deploy_master
+ end
+
+ it 'has the total time' do
+ expect(subject.production_events.first[:total_time]).not_to be_empty
+ end
+
+ it 'has a title' do
+ expect(subject.production_events.first[:title]).to eq(context.title)
+ end
+
+ it 'has the URL' do
+ expect(subject.production_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)
+ end
+
+ it 'has a created_at timestamp' do
+ expect(subject.production_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
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.production_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)
+ end
+ end
+
+ def setup(context)
+ milestone = create(:milestone, project: project)
+ context.update(milestone: milestone)
+ mr = create_merge_request_closing_issue(context)
+
+ ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.sha)
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb
new file mode 100644
index 00000000000..1c5c308da7d
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::IssueEvent do
+ 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_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb
new file mode 100644
index 00000000000..d76a255acf5
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::PlanEvent do
+ 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
new file mode 100644
index 00000000000..ac17e3b4287
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::ProductionEvent do
+ 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
new file mode 100644
index 00000000000..1ff53aa0227
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::ReviewEvent do
+ 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
new file mode 100644
index 00000000000..7019e4c3351
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+shared_examples 'default query config' do
+ let(:event) { described_class.new(project: double, options: {}) }
+
+ 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
+end
diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb
new file mode 100644
index 00000000000..4862d4765f2
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::StagingEvent do
+ 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
new file mode 100644
index 00000000000..e249db69fc6
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::TestEvent do
+ 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/updater_spec.rb b/spec/lib/gitlab/cycle_analytics/updater_spec.rb
new file mode 100644
index 00000000000..eff54cd3692
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/updater_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::Updater do
+ describe 'updates authors' do
+ let(:user) { create(:user) }
+ let(:events) { [{ 'author_id' => user.id }] }
+
+ it 'maps the correct user' do
+ described_class.update!(events, from: 'author_id', to: 'author', klass: User)
+
+ expect(events.first['author']).to eq(user)
+ end
+ end
+
+ describe 'updates builds' do
+ let(:build) { create(:ci_build) }
+ let(:events) { [{ 'id' => build.id }] }
+
+ it 'maps the correct build' do
+ described_class.update!(events, from: 'id', to: 'build', klass: ::Ci::Build)
+
+ expect(events.first['build']).to eq(build)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index 6f8e9a4be64..c7a0139d32a 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -206,5 +206,9 @@ describe Gitlab::Email::ReplyParser, lib: true do
it "properly renders email reply from MS Outlook client" do
expect(test_parse_body(fixture_file("emails/outlook.eml"))).to eq("Microsoft Outlook 2010")
end
+
+ it "properly renders html-only email from MS Outlook" do
+ expect(test_parse_body(fixture_file("emails/outlook_html.eml"))).to eq("Microsoft Outlook 2010")
+ end
end
end
diff --git a/spec/lib/light_url_builder_spec.rb b/spec/lib/light_url_builder_spec.rb
new file mode 100644
index 00000000000..a826b24419a
--- /dev/null
+++ b/spec/lib/light_url_builder_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe Gitlab::UrlBuilder, lib: true do
+ describe '.build' do
+ context 'when passing a Commit' do
+ it 'returns a proper URL' do
+ commit = build_stubbed(:commit)
+
+ url = described_class.build(commit)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}"
+ end
+ end
+
+ context 'when passing an Issue' do
+ it 'returns a proper URL' do
+ issue = build_stubbed(:issue, iid: 42)
+
+ url = described_class.build(issue)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}"
+ end
+ end
+
+ context 'when passing a MergeRequest' do
+ it 'returns a proper URL' do
+ merge_request = build_stubbed(:merge_request, iid: 42)
+
+ url = described_class.build(merge_request)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}"
+ end
+ end
+
+ context 'when passing a Note' do
+ context 'on a Commit' do
+ it 'returns a proper URL' do
+ note = build_stubbed(:note_on_commit)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
+ end
+ end
+
+ context 'on a Commit Diff' do
+ it 'returns a proper URL' do
+ note = build_stubbed(:diff_note_on_commit)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
+ end
+ end
+
+ context 'on an Issue' do
+ it 'returns a proper URL' do
+ issue = create(:issue, iid: 42)
+ note = build_stubbed(:note_on_issue, noteable: issue)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a MergeRequest' do
+ it 'returns a proper URL' do
+ merge_request = create(:merge_request, iid: 42)
+ note = build_stubbed(:note_on_merge_request, noteable: merge_request)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a MergeRequest Diff' do
+ it 'returns a proper URL' do
+ merge_request = create(:merge_request, iid: 42)
+ note = build_stubbed(:diff_note_on_merge_request, noteable: merge_request)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a ProjectSnippet' do
+ it 'returns a proper URL' do
+ project_snippet = create(:project_snippet)
+ note = build_stubbed(:note_on_project_snippet, noteable: project_snippet)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}"
+ end
+ end
+
+ context 'on another object' do
+ it 'returns a proper URL' do
+ project = build_stubbed(:project)
+
+ expect { described_class.build(project) }.
+ to raise_error(NotImplementedError, 'No URL builder defined for Project')
+ end
+ end
+ end
+
+ context 'when passing a WikiPage' do
+ it 'returns a proper URL' do
+ wiki_page = build(:wiki_page)
+ url = described_class.build(wiki_page)
+
+ expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}"
+ end
+ end
+ end
+end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index ae185de9ca3..ef07f2275b1 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -1052,4 +1052,132 @@ describe Ci::Build, models: true do
end
end
end
+
+ describe '#has_environment?' do
+ subject { build.has_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#starts_environment?' do
+ subject { build.starts_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'and start action is defined' do
+ before do
+ build.update(options: { environment: { action: 'start' } } )
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#stops_environment?' do
+ subject { build.stops_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and stop action is defined' do
+ before do
+ build.update(options: { environment: { action: 'stop' } } )
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#last_deployment' do
+ subject { build.last_deployment }
+
+ context 'when multiple deployments are created' do
+ let!(:deployment1) { create(:deployment, deployable: build) }
+ let!(:deployment2) { create(:deployment, deployable: build) }
+
+ it 'returns the latest one' do
+ is_expected.to eq(deployment2)
+ end
+ end
+ end
+
+ describe '#outdated_deployment?' do
+ subject { build.outdated_deployment? }
+
+ context 'when build succeeded' do
+ let(:build) { create(:ci_build, :success) }
+ let!(:deployment) { create(:deployment, deployable: build) }
+
+ context 'current deployment is latest' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'current deployment is not latest on environment' do
+ let!(:deployment2) { create(:deployment, environment: deployment.environment) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when build failed' do
+ let(:build) { create(:ci_build, :failed) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#expanded_environment_name' do
+ subject { build.expanded_environment_name }
+
+ context 'when environment uses variables' do
+ let(:build) { create(:ci_build, ref: 'master', environment: 'review/$CI_BUILD_REF_NAME') }
+
+ it { is_expected.to eq('review/master') }
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a37a00f461a..a7e90c8a381 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -4,6 +4,12 @@ describe Ci::Build, models: true do
let(:build) { create(:ci_build) }
let(:test_trace) { 'This is a test' }
+ it { is_expected.to belong_to(:runner) }
+ it { is_expected.to belong_to(:trigger_request) }
+ it { is_expected.to belong_to(:erased_by) }
+
+ it { is_expected.to have_many(:deployments) }
+
describe '#trace' do
it 'obfuscates project runners token' do
allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}")
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 2a87a411e9d..d8c47322220 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -86,17 +86,30 @@ describe JiraService, models: true do
project_key: 'GitLabProject'
)
+ # These stubs are needed to test JiraService#close_issue.
+ # We close the issue then do another request to API to check if it got closed.
+ # Here is stubbed the API return with a closed and an opened issues.
+ open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { "id" => "JIRA-123" })
+ closed_issue = open_issue.dup
+ allow(open_issue).to receive(:resolution).and_return(false)
+ allow(closed_issue).to receive(:resolution).and_return(true)
+ allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
+
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-123")
+
@jira_service.save
project_issues_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123'
@project_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject'
@transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
+ @remote_link_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/remotelink'
WebMock.stub_request(:get, @project_url)
WebMock.stub_request(:get, project_issues_url)
WebMock.stub_request(:post, @transitions_url)
WebMock.stub_request(:post, @comment_url)
+ WebMock.stub_request(:post, @remote_link_url)
end
it "calls JIRA API" do
@@ -107,6 +120,37 @@ describe JiraService, models: true do
).once
end
+ # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links
+ # for more information
+ it "creates Remote Link reference in JIRA for comment" do
+ @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
+ # Creates comment
+ expect(WebMock).to have_requested(:post, @comment_url)
+
+ # Creates Remote Link in JIRA issue fields
+ expect(WebMock).to have_requested(:post, @remote_link_url).with(
+ body: hash_including(
+ GlobalID: "GitLab",
+ object: {
+ url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{merge_request.diff_head_sha}",
+ title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.",
+ icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ status: { resolved: true, icon: { url16x16: "http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png", title: "Closed" } }
+ }
+ )
+ ).once
+ end
+
+ it "does not send comment or remote links to issues already closed" do
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true)
+
+ @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
+ expect(WebMock).not_to have_requested(:post, @comment_url)
+ expect(WebMock).not_to have_requested(:post, @remote_link_url)
+ end
+
it "references the GitLab commit/merge request" do
stub_config_setting(base_url: custom_base_url)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 2470d504c68..72ac41f3472 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1354,6 +1354,28 @@ describe Repository, models: true do
repository.add_tag(user, '8.5', 'master', 'foo')
end
+ it 'does not create a tag when a pre-hook fails' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
+
+ expect do
+ repository.add_tag(user, '8.5', 'master', 'foo')
+ end.to raise_error(GitHooksService::PreReceiveError)
+
+ repository.expire_tags_cache
+ expect(repository.find_tag('8.5')).to be_nil
+ end
+
+ it 'passes tag SHA to hooks' do
+ spy = GitHooksService.new
+ allow(GitHooksService).to receive(:new).and_return(spy)
+ allow(spy).to receive(:execute).and_call_original
+
+ tag = repository.add_tag(user, '8.5', 'master', 'foo')
+
+ expect(spy).to have_received(:execute).
+ with(anything, anything, anything, tag.target, anything)
+ end
+
it 'returns a Gitlab::Git::Tag object' do
tag = repository.add_tag(user, '8.5', 'master', 'foo')
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index d91628807ef..e88a7e27d45 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -5,7 +5,7 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:key) { create(:key, user: user) }
let(:project) { create(:project) }
- let(:secret_token) { File.read Gitlab.config.gitlab_shell.secret_file }
+ let(:secret_token) { Gitlab::Shell.secret_token }
describe "GET /internal/check", no_db: true do
it do
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
new file mode 100644
index 00000000000..705dbb7d1c0
--- /dev/null
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe 'cycle analytics events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
+
+ describe 'GET /:namespace/:project/cycle_analytics/events/issues' do
+ before do
+ project.team << [user, :developer]
+
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+
+ 3.times { create_cycle }
+ deploy_master
+
+ login_as(user)
+ end
+
+ it 'lists the issue events' do
+ get namespace_project_cycle_analytics_issue_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ first_issue_iid = Issue.order(created_at: :desc).pluck(:iid).first.to_s
+
+ expect(json_response['events'].first['iid']).to eq(first_issue_iid)
+ end
+
+ it 'lists the plan events' do
+ get namespace_project_cycle_analytics_plan_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ expect(json_response['events'].first['short_sha']).to eq(MergeRequest.last.commits.first.short_id)
+ end
+
+ it 'lists the code events' do
+ get namespace_project_cycle_analytics_code_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ first_mr_iid = MergeRequest.order(created_at: :desc).pluck(:iid).first.to_s
+
+ expect(json_response['events'].first['iid']).to eq(first_mr_iid)
+ end
+
+ it 'lists the test events' do
+ get namespace_project_cycle_analytics_test_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ expect(json_response['events'].first['date']).not_to be_empty
+ end
+
+ it 'lists the review events' do
+ get namespace_project_cycle_analytics_review_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ first_mr_iid = MergeRequest.order(created_at: :desc).pluck(:iid).first.to_s
+
+ expect(json_response['events'].first['iid']).to eq(first_mr_iid)
+ end
+
+ it 'lists the staging events' do
+ get namespace_project_cycle_analytics_staging_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ expect(json_response['events'].first['date']).not_to be_empty
+ end
+
+ it 'lists the production events' do
+ get namespace_project_cycle_analytics_production_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ first_issue_iid = Issue.order(created_at: :desc).pluck(:iid).first.to_s
+
+ expect(json_response['events'].first['iid']).to eq(first_issue_iid)
+ end
+
+ context 'specific branch' do
+ it 'lists the test events' do
+ branch = MergeRequest.first.source_branch
+
+ get namespace_project_cycle_analytics_test_path(project.namespace, project, format: :json, branch: branch)
+
+ expect(json_response['events']).not_to be_empty
+
+ expect(json_response['events'].first['date']).not_to be_empty
+ end
+ end
+
+ context 'with private project and builds' do
+ before do
+ ProjectMember.first.update(access_level: Gitlab::Access::GUEST)
+ end
+
+ it 'does not list the test events' do
+ get namespace_project_cycle_analytics_test_path(project.namespace, project, format: :json)
+
+ expect(response).to have_http_status(:not_found)
+ end
+
+ it 'does not list the staging events' do
+ get namespace_project_cycle_analytics_staging_path(project.namespace, project, format: :json)
+
+ expect(response).to have_http_status(:not_found)
+ end
+
+ it 'lists the issue events' do
+ get namespace_project_cycle_analytics_issue_path(project.namespace, project, format: :json)
+
+ expect(response).to have_http_status(:ok)
+ end
+ end
+ end
+
+ def json_response
+ JSON.parse(response.body)
+ end
+
+ def create_cycle
+ milestone = create(:milestone, project: project)
+ issue.update(milestone: milestone)
+ mr = create_merge_request_closing_issue(issue)
+
+ pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha)
+ pipeline.run
+
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ merge_merge_requests_closing_issue(issue)
+
+ ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.sha)
+ end
+end
diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb
new file mode 100644
index 00000000000..9ac6f20fd3c
--- /dev/null
+++ b/spec/serializers/analytics_build_entity_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe AnalyticsBuildEntity do
+ let(:entity) do
+ described_class.new(build, request: double)
+ end
+
+ context 'build with an author' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, author: user) }
+
+ subject { entity.as_json }
+
+ it 'contains the URL' do
+ expect(subject).to include(:url)
+ end
+
+ it 'contains the author' do
+ expect(subject).to include(:author)
+ end
+
+ it 'does not contain sensitive information' do
+ expect(subject).not_to include(/token/)
+ expect(subject).not_to include(/variables/)
+ end
+ end
+end
diff --git a/spec/serializers/analytics_build_serializer_spec.rb b/spec/serializers/analytics_build_serializer_spec.rb
new file mode 100644
index 00000000000..a0a9d9a5f12
--- /dev/null
+++ b/spec/serializers/analytics_build_serializer_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe AnalyticsBuildSerializer do
+ let(:serializer) do
+ described_class
+ .new.represent(resource)
+ end
+
+ let(:json) { serializer.as_json }
+ let(:resource) { create(:ci_build) }
+
+ context 'when there is a single object provided' do
+ it 'it generates payload for single object' do
+ expect(json).to be_an_instance_of Hash
+ end
+
+ it 'contains important elements of analyticsBuild' do
+ expect(json)
+ .to include(:name, :branch, :short_sha, :date, :total_time, :url, :author)
+ end
+ end
+end
diff --git a/spec/serializers/analytics_generic_entity_spec.rb b/spec/serializers/analytics_generic_entity_spec.rb
new file mode 100644
index 00000000000..68086216ba9
--- /dev/null
+++ b/spec/serializers/analytics_generic_entity_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe AnalyticsIssueEntity do
+ let(:user) { create(:user) }
+ let(:entity_hash) do
+ {
+ total_time: "172802.724419",
+ title: "Eos voluptatem inventore in sed.",
+ iid: "1",
+ id: "1",
+ created_at: "2016-11-12 15:04:02.948604",
+ author: user,
+ }
+ end
+
+ let(:project) { create(:empty_project) }
+ let(:request) { EntityRequest.new(project: project, entity: :merge_request) }
+
+ let(:entity) do
+ described_class.new(entity_hash, request: request, project: project)
+ end
+
+ context 'generic entity' do
+ subject { entity.as_json }
+
+ it 'contains the entity URL' do
+ expect(subject).to include(:url)
+ end
+
+ it 'contains the author' do
+ expect(subject).to include(:author)
+ end
+
+ it 'does not contain sensitive information' do
+ expect(subject).not_to include(/token/)
+ expect(subject).not_to include(/variables/)
+ end
+ end
+end
diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb
new file mode 100644
index 00000000000..2842e1ba52f
--- /dev/null
+++ b/spec/serializers/analytics_issue_serializer_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe AnalyticsIssueSerializer do
+ let(:serializer) do
+ described_class
+ .new(project: project, entity: :merge_request)
+ .represent(resource)
+ end
+
+ let(:user) { create(:user) }
+ let(:json) { serializer.as_json }
+ let(:project) { create(:project) }
+ let(:resource) do
+ {
+ total_time: "172802.724419",
+ title: "Eos voluptatem inventore in sed.",
+ iid: "1",
+ id: "1",
+ created_at: "2016-11-12 15:04:02.948604",
+ author: user,
+ }
+ end
+
+ context 'when there is a single object provided' do
+ it 'it generates payload for single object' do
+ expect(json).to be_an_instance_of Hash
+ end
+
+ it 'contains important elements of the issue' do
+ expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author)
+ end
+ end
+end
diff --git a/spec/serializers/analytics_merge_request_serializer_spec.rb b/spec/serializers/analytics_merge_request_serializer_spec.rb
new file mode 100644
index 00000000000..564207984df
--- /dev/null
+++ b/spec/serializers/analytics_merge_request_serializer_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe AnalyticsMergeRequestSerializer do
+ let(:serializer) do
+ described_class
+ .new(project: project, entity: :merge_request)
+ .represent(resource)
+ end
+
+ let(:user) { create(:user) }
+ let(:json) { serializer.as_json }
+ let(:project) { create(:project) }
+ let(:resource) do
+ {
+ total_time: "172802.724419",
+ title: "Eos voluptatem inventore in sed.",
+ iid: "1",
+ id: "1",
+ state: 'open',
+ created_at: "2016-11-12 15:04:02.948604",
+ author: user
+ }
+ end
+
+ context 'when there is a single object provided' do
+ it 'it generates payload for single object' do
+ expect(json).to be_an_instance_of Hash
+ end
+
+ it 'contains important elements of the merge request' do
+ expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author, :state)
+ end
+ end
+end
diff --git a/spec/serializers/entity_date_helper_spec.rb b/spec/serializers/entity_date_helper_spec.rb
new file mode 100644
index 00000000000..b9cc2f64831
--- /dev/null
+++ b/spec/serializers/entity_date_helper_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe EntityDateHelper do
+ let(:date_helper_class) { Class.new { include EntityDateHelper }.new }
+
+ it 'converts 0 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(0)).to eq(seconds: 0)
+ end
+
+ it 'converts 40 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(40)).to eq(seconds: 40)
+ end
+
+ it 'converts 60 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(60)).to eq(mins: 1)
+ end
+
+ it 'converts 70 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(70)).to eq(mins: 1, seconds: 10)
+ end
+
+ it 'converts 3600 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(3600)).to eq(hours: 1)
+ end
+
+ it 'converts 3750 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(3750)).to eq(hours: 1, mins: 2, seconds: 30)
+ end
+
+ it 'converts 86400 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(86400)).to eq(days: 1)
+ end
+
+ it 'converts 86560 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(86560)).to eq(days: 1, mins: 2, seconds: 40)
+ end
+
+ it 'converts 86760 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(99760)).to eq(days: 1, hours: 3, mins: 42, seconds: 40)
+ end
+
+ it 'converts 986760 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(986760)).to eq(days: 11, hours: 10, mins: 6)
+ end
+end
diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb
index da724643604..538e85cdc89 100644
--- a/spec/services/destroy_group_service_spec.rb
+++ b/spec/services/destroy_group_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe DestroyGroupService, services: true do
+ include DatabaseConnectionHelpers
+
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:project) { create(:project, namespace: group) }
@@ -50,6 +52,44 @@ describe DestroyGroupService, services: true do
describe 'asynchronous delete' do
it_behaves_like 'group destruction', true
+
+ context 'potential race conditions' do
+ context "when the `GroupDestroyWorker` task runs immediately" do
+ it "deletes the group" do
+ # Commit the contents of this spec's transaction so far
+ # so subsequent db connections can see it.
+ #
+ # DO NOT REMOVE THIS LINE, even if you see a WARNING with "No
+ # transaction is currently in progress". Without this, this
+ # spec will always be green, since the group created in setup
+ # cannot be seen by any other connections / threads in this spec.
+ Group.connection.commit_db_transaction
+
+ group_record = run_with_new_database_connection do |conn|
+ conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
+ end
+
+ expect(group_record).not_to be_nil
+
+ # Execute the contents of `GroupDestroyWorker` in a separate thread, to
+ # simulate data manipulation by the Sidekiq worker (different database
+ # connection / transaction).
+ expect(GroupDestroyWorker).to receive(:perform_async).and_wrap_original do |m, group_id, user_id|
+ Thread.new { m[group_id, user_id] }.join(5)
+ end
+
+ # Kick off the initial group destroy in a new thread, so that
+ # it doesn't share this spec's database transaction.
+ Thread.new { DestroyGroupService.new(group, user).async_execute }.join(5)
+
+ group_record = run_with_new_database_connection do |conn|
+ conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
+ end
+
+ expect(group_record).to be_nil
+ end
+ end
+ end
end
describe 'synchronous delete' do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 6358f1fed71..62f9982e840 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -492,6 +492,16 @@ describe GitPushService, services: true do
let(:message) { "this is some work.\n\ncloses JIRA-1" }
let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://#{Gitlab.config.gitlab.host}/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json }
+ before do
+ open_issue = JIRA::Resource::Issue.new(jira_tracker.client, attrs: { "id" => "JIRA-1" })
+ closed_issue = open_issue.dup
+ allow(open_issue).to receive(:resolution).and_return(false)
+ allow(closed_issue).to receive(:resolution).and_return(true)
+ allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
+
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-1")
+ end
+
context "using right markdown" do
it "initiates one api call to jira server to close the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index f93d7732a9a..1fd9f5a4910 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -67,17 +67,19 @@ describe MergeRequests::MergeService, services: true do
it 'closes issues on JIRA issue tracker' do
jira_issue = ExternalIssue.new('JIRA-123', project)
+ stub_jira_urls(jira_issue)
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
- expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once
+ expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, an_instance_of(JIRA::Resource::Issue)).once
service.execute(merge_request)
end
context "wrong issue markdown" do
it 'does not close issues on JIRA issue tracker' do
- jira_issue = ExternalIssue.new('#123', project)
+ jira_issue = ExternalIssue.new('#JIRA-123', project)
+ stub_jira_urls(jira_issue)
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5bb107fdd85..56d39e9a005 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe SystemNoteService, services: true do
+ include Gitlab::Routing.url_helpers
+
let(:project) { create(:project) }
let(:author) { create(:user) }
let(:noteable) { create(:issue, project: project) }
@@ -543,23 +545,55 @@ describe SystemNoteService, services: true do
before { stub_jira_urls(jira_issue.id) }
- context 'in JIRA issue tracker' do
+ context 'in issue' do
before { jira_service_settings }
describe "new reference" do
subject { described_class.cross_reference(jira_issue, commit, author) }
it { is_expected.to eq(success_message) }
+
+ it "creates remote link" do
+ subject
+
+ expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
+ body: hash_including(
+ GlobalID: "GitLab",
+ object: {
+ url: namespace_project_commit_url(project.namespace, project, commit),
+ title: "GitLab: Mentioned on commit - #{commit.title}",
+ icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ status: { resolved: false }
+ }
+ )
+ ).once
+ end
end
end
- context 'issue from an issue' do
+ context 'in commit' do
context 'in JIRA issue tracker' do
before { jira_service_settings }
subject { described_class.cross_reference(jira_issue, issue, author) }
it { is_expected.to eq(success_message) }
+
+ it "creates remote link" do
+ subject
+
+ expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
+ body: hash_including(
+ GlobalID: "GitLab",
+ object: {
+ url: namespace_project_issue_url(project.namespace, project, issue),
+ title: "GitLab: Mentioned on issue - #{issue.title}",
+ icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ status: { resolved: false }
+ }
+ )
+ ).once
+ end
end
end
@@ -572,6 +606,13 @@ describe SystemNoteService, services: true do
subject { described_class.cross_reference(jira_issue, commit, author) }
it { is_expected.not_to eq(success_message) }
+
+ it 'does not try to create comment and remote link' do
+ subject
+
+ expect(WebMock).not_to have_requested(:post, jira_api_comment_url(jira_issue))
+ expect(WebMock).not_to have_requested(:post, jira_api_remote_link_url(jira_issue))
+ end
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 73cf4c9a24c..bead1a006d1 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -26,10 +26,11 @@ RSpec.configure do |config|
config.verbose_retry = true
config.display_try_failure_messages = true
- config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include Devise::Test::ControllerHelpers, type: :view
config.include Warden::Test::Helpers, type: :request
- config.include LoginHelpers, type: :feature
- config.include SearchHelpers, type: :feature
+ config.include LoginHelpers, type: :feature
+ config.include SearchHelpers, type: :feature
config.include StubConfiguration
config.include EmailHelpers
config.include TestEnv
diff --git a/spec/support/database_connection_helpers.rb b/spec/support/database_connection_helpers.rb
new file mode 100644
index 00000000000..763329499f0
--- /dev/null
+++ b/spec/support/database_connection_helpers.rb
@@ -0,0 +1,9 @@
+module DatabaseConnectionHelpers
+ def run_with_new_database_connection
+ pool = ActiveRecord::Base.connection_pool
+ conn = pool.checkout
+ yield conn
+ ensure
+ pool.checkin(conn)
+ end
+end
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index 96e0dad6b55..7437ba2688d 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -57,6 +57,10 @@ module JiraServiceHelper
JIRA_API + "/issue/#{issue_id}/comment"
end
+ def jira_api_remote_link_url(issue_id)
+ JIRA_API + "/issue/#{issue_id}/remotelink"
+ end
+
def jira_api_transition_url(issue_id)
JIRA_API + "/issue/#{issue_id}/transitions"
end
@@ -75,6 +79,7 @@ module JiraServiceHelper
WebMock.stub_request(:get, jira_issue_url(issue_id))
WebMock.stub_request(:get, jira_api_test_url)
WebMock.stub_request(:post, jira_api_comment_url(issue_id))
+ WebMock.stub_request(:post, jira_api_remote_link_url(issue_id))
WebMock.stub_request(:post, jira_api_transition_url(issue_id))
end
end
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index da43622d3f9..e0c77201116 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -1,14 +1,12 @@
require 'spec_helper'
-describe 'projects/builds/show' do
- include Devise::Test::ControllerHelpers
-
+describe 'projects/builds/show', :view do
let(:project) { create(:project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
let(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id)
+ create(:ci_pipeline, project: project, sha: project.commit.id)
end
- let(:build) { create(:ci_build, pipeline: pipeline) }
before do
assign(:build, build)
@@ -17,6 +15,129 @@ describe 'projects/builds/show' do
allow(view).to receive(:can?).and_return(true)
end
+ describe 'environment info in build view' do
+ context 'build with latest deployment' do
+ let(:build) do
+ create(:ci_build, :success, environment: 'staging')
+ end
+
+ before do
+ create(:environment, name: 'staging')
+ create(:deployment, deployable: build)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is the most recent deployment'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build with outdated deployment' do
+ let(:build) do
+ create(:ci_build, :success, environment: 'staging', pipeline: pipeline)
+ end
+
+ let(:second_build) do
+ create(:ci_build, :success, environment: 'staging', pipeline: pipeline)
+ end
+
+ let(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ let!(:first_deployment) do
+ create(:deployment, environment: environment, deployable: build)
+ end
+
+ let!(:second_deployment) do
+ create(:deployment, environment: environment, deployable: second_build)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is an out-of-date deployment ' \
+ "to staging.\nView the most recent deployment ##{second_deployment.iid}."
+ render
+
+ expect(rendered).to have_css('.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build failed to deploy' do
+ let(:build) do
+ create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
+ end
+
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'The deployment of this build to staging did not succeed.'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build will deploy' do
+ let(:build) do
+ create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
+ end
+
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is creating a deployment to staging'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build that failed to deploy and environment has not been created' do
+ let(:build) do
+ create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
+ end
+
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'The deployment of this build to staging did not succeed'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build that will deploy and environment has not been created' do
+ let(:build) do
+ create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
+ end
+
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is creating a deployment to staging'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+ end
+
context 'when build is running' do
before do
build.run!
diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb
index 2c9e7c2cd02..2d47d93acec 100644
--- a/spec/workers/pipeline_metrics_worker_spec.rb
+++ b/spec/workers/pipeline_metrics_worker_spec.rb
@@ -15,32 +15,36 @@ describe PipelineMetricsWorker do
end
describe '#perform' do
- subject { described_class.new.perform(pipeline.id) }
+ before do
+ described_class.new.perform(pipeline.id)
+ end
context 'when pipeline is running' do
let(:status) { 'running' }
it 'records the build start time' do
- subject
-
expect(merge_request.reload.metrics.latest_build_started_at).to be_like_time(pipeline.started_at)
end
it 'clears the build end time' do
- subject
-
expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
end
+
+ it 'records the pipeline' do
+ expect(merge_request.reload.metrics.pipeline).to eq(pipeline)
+ end
end
context 'when pipeline succeeded' do
let(:status) { 'success' }
it 'records the build end time' do
- subject
-
expect(merge_request.reload.metrics.latest_build_finished_at).to be_like_time(pipeline.finished_at)
end
+
+ it 'records the pipeline' do
+ expect(merge_request.reload.metrics.pipeline).to eq(pipeline)
+ end
end
end
end