diff options
39 files changed, 593 insertions, 350 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3340ca9d64c..700e2232da1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34" +image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34" stages: - sync diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 7deb4f00a84..e6b81c9f366 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -38,7 +38,7 @@ POSTGRES_HOST_AUTH_METHOD: trust .use-pg10: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34" services: - name: postgres:10.12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] @@ -47,7 +47,7 @@ POSTGRES_HOST_AUTH_METHOD: trust .use-pg11: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34" services: - name: postgres:11.6 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] @@ -65,7 +65,7 @@ POSTGRES_HOST_AUTH_METHOD: trust .use-pg10-ee: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34" services: - name: postgres:10.12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] @@ -75,7 +75,7 @@ POSTGRES_HOST_AUTH_METHOD: trust .use-pg11-ee: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34" services: - name: postgres:11.6 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] diff --git a/.rubocop.yml b/.rubocop.yml index ee840476c8a..4262fd9ca74 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -400,8 +400,6 @@ RSpec/RepeatedExample: - 'spec/lib/gitlab/closing_issue_extractor_spec.rb' - 'spec/lib/gitlab/danger/changelog_spec.rb' - 'spec/lib/gitlab/import_export/project/relation_factory_spec.rb' - - 'spec/routing/admin_routing_spec.rb' - 'spec/rubocop/cop/migration/update_large_table_spec.rb' - 'spec/services/notification_service_spec.rb' - 'spec/services/web_hook_service_spec.rb' - - 'ee/spec/services/geo/repository_verification_primary_service_spec.rb' diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index d4ee59790e9..79f203091f2 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -121,6 +121,7 @@ } > .btn, + > .btn-group, > .btn-container, > .dropdown, > input, @@ -161,7 +162,8 @@ .dropdown, .dropdown-toggle, .dropdown-menu-toggle, - .form-control { + .form-control, + > .btn-group { margin: 0 0 $gl-padding-8; display: block; width: 100%; diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index fac058e5a46..fa5a79cc12b 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -117,20 +117,20 @@ module Milestoneish false end - def total_issue_time_spent - @total_issue_time_spent ||= issues.joins(:timelogs).sum(:time_spent) + def total_time_spent + @total_time_spent ||= issues.joins(:timelogs).sum(:time_spent) + merge_requests.joins(:timelogs).sum(:time_spent) end - def human_total_issue_time_spent - Gitlab::TimeTrackingFormatter.output(total_issue_time_spent) + def human_total_time_spent + Gitlab::TimeTrackingFormatter.output(total_time_spent) end - def total_issue_time_estimate - @total_issue_time_estimate ||= issues.sum(:time_estimate) + def total_time_estimate + @total_time_estimate ||= issues.sum(:time_estimate) + merge_requests.sum(:time_estimate) end - def human_total_issue_time_estimate - Gitlab::TimeTrackingFormatter.output(total_issue_time_estimate) + def human_total_time_estimate + Gitlab::TimeTrackingFormatter.output(total_time_estimate) end private diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 65fd5c1b35a..d0cec0e9fc6 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -94,7 +94,7 @@ class GlobalMilestone end def merge_requests - @merge_requests ||= MergeRequest.of_milestones(milestone).includes(:target_project, :assignee, :labels) + @merge_requests ||= MergeRequest.of_milestones(milestone).includes(:target_project, :assignees, :labels) end def labels diff --git a/app/services/ci/daily_report_result_service.rb b/app/services/ci/daily_report_result_service.rb index 79b5015c076..b774a806203 100644 --- a/app/services/ci/daily_report_result_service.rb +++ b/app/services/ci/daily_report_result_service.rb @@ -19,12 +19,21 @@ module Ci last_pipeline_id: pipeline.id } - pipeline.builds.with_coverage.map do |build| + aggregate(pipeline.builds.with_coverage).map do |group_name, group| base_attrs.merge( - title: build.group_name, - value: build.coverage + title: group_name, + value: average_coverage(group) ) end end + + def aggregate(builds) + builds.group_by(&:group_name) + end + + def average_coverage(group) + total_coverage = group.reduce(0.0) { |sum, build| sum + build.coverage } + (total_coverage / group.size).round(2) + end end end diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index f11c730eba6..3fa957f38a0 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -23,7 +23,7 @@ .prepend-top-20 %button.btn.btn-success.js-ci-variables-save-button{ type: 'button' } %span.hide.js-ci-variables-save-loading-icon - = icon('spinner spin') + .spinner.spinner-light.mr-1 = _('Save variables') %button.btn.btn-info.btn-inverted.prepend-left-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@variables.size == 0}" } } - if @variables.size == 0 diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index d81089bee68..c347b8d2c9c 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -6,7 +6,7 @@ - if show_feed_buttons = render 'shared/issuable/feed_buttons' - .btn-group.append-right-10< + .btn-group - if show_export_button = render_if_exists 'projects/issues/export_csv/button' diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml index 78c561e81ef..0a352d26b0b 100644 --- a/app/views/projects/issues/import_csv/_button.html.haml +++ b/app/views/projects/issues/import_csv/_button.html.haml @@ -1,11 +1,22 @@ - type = local_assigns.fetch(:type, :icon) -%button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon), - data: { toggle: 'modal', target: '.issues-import-modal' } } - - if type == :icon - = sprite_icon('import') - - else - = _('Import CSV') - - if Feature.enabled?(:jira_issue_import, @project) - = link_to _("Import Jira issues"), project_import_jira_path(@project), class: "btn btn-default" + .dropdown.btn-group + %button.btn.rounded-right.text-center{ class: ('has-tooltip' if type == :icon), title: (_('Import issues') if type == :icon), + data: { toggle: 'dropdown' }, 'aria-label' => _('Import issues'), 'aria-haspopup' => 'true', 'aria-expanded' => 'false' } + - if type == :icon + = sprite_icon('import') + - else + = _('Import issues') + %ul.dropdown-menu + %li + %button.btn{ data: { toggle: 'modal', target: '.issues-import-modal' } } + = _('Import CSV') + %li= link_to _('Import from Jira'), project_import_jira_path(@project) +- else + %button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon), + data: { toggle: 'modal', target: '.issues-import-modal' } } + - if type == :icon + = sprite_icon('import') + - else + = _('Import CSV') diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index aa9c4be1cc1..ba1629bd99a 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -93,10 +93,10 @@ = milestone.issues_visible_to_user(current_user).closed.count .block - #issuable-time-tracker{ data: { time_estimate: @milestone.total_issue_time_estimate, - time_spent: @milestone.total_issue_time_spent, - human_time_estimate: @milestone.human_total_issue_time_estimate, - human_time_spent: @milestone.human_total_issue_time_spent, + #issuable-time-tracker{ data: { time_estimate: @milestone.total_time_estimate, + time_spent: @milestone.total_time_spent, + human_time_estimate: @milestone.human_total_time_estimate, + human_time_spent: @milestone.human_total_time_spent, limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s } } = render_if_exists 'shared/milestones/weight', milestone: milestone diff --git a/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-views-ci-variables.yml b/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-views-ci-variables.yml new file mode 100644 index 00000000000..a6e90272a8f --- /dev/null +++ b/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-views-ci-variables.yml @@ -0,0 +1,5 @@ +--- +title: Migrate .fa-spinner to .spinner for app/views/ci/variables +merge_request: 25030 +author: nuwe1 +type: other diff --git a/changelogs/unreleased/admin-routing-spec.yml b/changelogs/unreleased/admin-routing-spec.yml new file mode 100644 index 00000000000..693aa36bf21 --- /dev/null +++ b/changelogs/unreleased/admin-routing-spec.yml @@ -0,0 +1,5 @@ +--- +title: Remove duplicate show spec in admin routing +merge_request: 28790 +author: Rajendra Kadam +type: changed diff --git a/changelogs/unreleased/eb-fix-daily-report-results-upsert.yml b/changelogs/unreleased/eb-fix-daily-report-results-upsert.yml new file mode 100644 index 00000000000..f1578d8ba27 --- /dev/null +++ b/changelogs/unreleased/eb-fix-daily-report-results-upsert.yml @@ -0,0 +1,6 @@ +--- +title: Fix daily report result to use average of coverage values if there are multiple builds for a given group + name +merge_request: 28556 +author: +type: fixed diff --git a/changelogs/unreleased/include-mr-times.yml b/changelogs/unreleased/include-mr-times.yml new file mode 100755 index 00000000000..0e2e3a64dd5 --- /dev/null +++ b/changelogs/unreleased/include-mr-times.yml @@ -0,0 +1,5 @@ +--- +title: Include MR times in Milestone time overview +merge_request: 28519 +author: Bob van de Vijver +type: fixed diff --git a/changelogs/unreleased/validate-dynamic-pipeline-dependencies.yml b/changelogs/unreleased/validate-dynamic-pipeline-dependencies.yml new file mode 100644 index 00000000000..76a75022eab --- /dev/null +++ b/changelogs/unreleased/validate-dynamic-pipeline-dependencies.yml @@ -0,0 +1,5 @@ +--- +title: Validate dependency on job generating a CI config when using dynamic child pipelines +merge_request: 27916 +author: +type: added diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index 99abf5d3864..c53c46bf0cb 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -31,6 +31,16 @@ It's possible that this limit will be changed to a lower number in the future. - **Max size:** ~1 million characters / ~1 MB +## Number of issues in the milestone overview + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/39453) in GitLab 12.10. + +The maximum number of issues loaded on the milestone overview page is 3000. +When the number exceeds the limit the page displays an alert and links to a paginated +[issue list](../user/project/issues/index.md#issues-list) of all issues in the milestone. + +- **Limit:** 3000 issues + ## Number of pipelines per Git push > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/51401) in GitLab 11.10. diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md index b39e0b6e540..2bc897901fa 100644 --- a/doc/ci/parent_child_pipelines.md +++ b/doc/ci/parent_child_pipelines.md @@ -136,12 +136,11 @@ your own script to generate a YAML file, which is then [used to trigger a child This technique can be very powerful in generating pipelines targeting content that changed or to build a matrix of targets and architectures. +In GitLab 12.9, the child pipeline could fail to be created in certain cases, causing the parent pipeline to fail. +This is [resolved in GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/209070). + ## Limitations A parent pipeline can trigger many child pipelines, but a child pipeline cannot trigger further child pipelines. See the [related issue](https://gitlab.com/gitlab-org/gitlab/issues/29651) for discussion on possible future improvements. - -When triggering dynamic child pipelines, if the job containing the CI config artifact is not a predecessor of the -trigger job, the child pipeline will fail to be created, causing also the parent pipeline to fail. -In the future we want to validate the trigger job's dependencies [at the time the parent pipeline is created](https://gitlab.com/gitlab-org/gitlab/-/issues/209070) rather than when the child pipeline is created. diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md index 89c5522f4c4..085c1bd143e 100644 --- a/doc/user/project/milestones/index.md +++ b/doc/user/project/milestones/index.md @@ -133,7 +133,7 @@ The milestone sidebar on the milestone view shows the following: - Percentage complete, which is calculated as number of closed issues divided by total number of issues. - The start date and due date. -- The total time spent on all issues assigned to the milestone. +- The total time spent on all issues and merge requests assigned to the milestone. - The total issue weight of all issues assigned to the milestone. ![Project milestone page](img/milestones_project_milestone_page.png) diff --git a/lib/gitlab/ci/config/entry/include.rb b/lib/gitlab/ci/config/entry/include.rb index cd09d83b728..b2586714636 100644 --- a/lib/gitlab/ci/config/entry/include.rb +++ b/lib/gitlab/ci/config/entry/include.rb @@ -15,6 +15,11 @@ module Gitlab validations do validates :config, hash_or_string: true validates :config, allowed_keys: ALLOWED_KEYS + validate do + if config[:artifact] && config[:job].blank? + errors.add(:config, "must specify the job where to fetch the artifact from") + end + end end end end diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index 764047dae6d..4b0062549f0 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -142,6 +142,7 @@ module Gitlab validate_job_stage!(name, job) validate_job_dependencies!(name, job) validate_job_needs!(name, job) + validate_dynamic_child_pipeline_dependencies!(name, job) validate_job_environment!(name, job) end end @@ -163,37 +164,52 @@ module Gitlab def validate_job_dependencies!(name, job) return unless job[:dependencies] - stage_index = @stages.index(job[:stage]) - job[:dependencies].each do |dependency| - raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym] + validate_job_dependency!(name, dependency) + end + end - dependency_stage_index = @stages.index(@jobs[dependency.to_sym][:stage]) + def validate_dynamic_child_pipeline_dependencies!(name, job) + return unless includes = job.dig(:trigger, :include) - unless dependency_stage_index.present? && dependency_stage_index < stage_index - raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages" - end + includes.each do |included| + next unless dependency = included[:job] + + validate_job_dependency!(name, dependency) end end def validate_job_needs!(name, job) - return unless job.dig(:needs, :job) - - stage_index = @stages.index(job[:stage]) + return unless needs = job.dig(:needs, :job) - job.dig(:needs, :job).each do |need| - need_job_name = need[:name] + needs.each do |need| + dependency = need[:name] + validate_job_dependency!(name, dependency, 'need') + end + end - raise ValidationError, "#{name} job: undefined need: #{need_job_name}" unless @jobs[need_job_name.to_sym] + def validate_job_dependency!(name, dependency, dependency_type = 'dependency') + unless @jobs[dependency.to_sym] + raise ValidationError, "#{name} job: undefined #{dependency_type}: #{dependency}" + end - needs_stage_index = @stages.index(@jobs[need_job_name.to_sym][:stage]) + job_stage_index = stage_index(name) + dependency_stage_index = stage_index(dependency) - unless needs_stage_index.present? && needs_stage_index < stage_index - raise ValidationError, "#{name} job: need #{need_job_name} is not defined in prior stages" - end + # A dependency might be defined later in the configuration + # with a stage that does not exist + unless dependency_stage_index.present? && dependency_stage_index < job_stage_index + raise ValidationError, "#{name} job: #{dependency_type} #{dependency} is not defined in prior stages" end end + def stage_index(name) + job = @jobs[name.to_sym] + return unless job + + @stages.index(job[:stage]) + end + def validate_job_environment!(name, job) return unless job[:environment] return unless job[:environment].is_a?(Hash) diff --git a/lib/gitlab/database/batch_count.rb b/lib/gitlab/database/batch_count.rb index e0f6b0f9eee..5987dc34801 100644 --- a/lib/gitlab/database/batch_count.rb +++ b/lib/gitlab/database/batch_count.rb @@ -39,8 +39,8 @@ module Gitlab SLEEP_TIME_IN_SECONDS = 0.01 # 10 msec sleep # Each query should take < 500ms https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705 - DEFAULT_DISTINCT_BATCH_SIZE = 100_000 - DEFAULT_BATCH_SIZE = 10_000 + DEFAULT_DISTINCT_BATCH_SIZE = 10_000 + DEFAULT_BATCH_SIZE = 100_000 def initialize(relation, column: nil) @relation = relation diff --git a/lib/gitlab/jira_import/issue_serializer.rb b/lib/gitlab/jira_import/issue_serializer.rb index 0be5d22065a..cdcb62ac6e9 100644 --- a/lib/gitlab/jira_import/issue_serializer.rb +++ b/lib/gitlab/jira_import/issue_serializer.rb @@ -4,12 +4,14 @@ module Gitlab module JiraImport class IssueSerializer attr_reader :jira_issue, :project, :params, :formatter + attr_accessor :metadata def initialize(project, jira_issue, params = {}) @jira_issue = jira_issue @project = project @params = params @formatter = Gitlab::ImportFormatter.new + @metadata = [] end def execute @@ -36,6 +38,7 @@ module Gitlab body << formatter.author_line(jira_issue.reporter.displayName) body << formatter.assignee_line(jira_issue.assignee.displayName) if jira_issue.assignee body << jira_issue.description + body << add_metadata body.join end @@ -48,6 +51,50 @@ module Gitlab Issuable::STATE_ID_MAP[:opened] end end + + def add_metadata + add_field(%w(issuetype name), 'Issue type') + add_field(%w(priority name), 'Priority') + add_labels + add_field('environment', 'Environment') + add_field('duedate', 'Due date') + add_parent + add_versions + + return if metadata.empty? + + metadata.join("\n").prepend("\n\n---\n\n**Issue metadata**\n\n") + end + + def add_field(keys, field_label) + value = fields.dig(*keys) + return if value.blank? + + metadata << "- #{field_label}: #{value}" + end + + def add_labels + return if fields['labels'].blank? + + metadata << "- Labels: #{fields['labels'].join(', ')}" + end + + def add_parent + parent_issue_key = fields.dig('parent', 'key') + return if parent_issue_key.blank? + + metadata << "- Parent issue: [#{parent_issue_key}] #{fields['parent']['fields']['summary']}" + end + + def add_versions + return if fields['fixVersions'].blank? + + metadata << "- Fix versions: #{fields['fixVersions'].map { |version| version['name'] }.join(', ')}" + end + + def fields + jira_issue.fields + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 278aca1bf86..b9f0e1f7b8b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10815,9 +10815,6 @@ msgstr "" msgid "Import CSV" msgstr "" -msgid "Import Jira issues" -msgstr "" - msgid "Import Projects from Gitea" msgstr "" @@ -10833,6 +10830,9 @@ msgstr "" msgid "Import an exported GitLab project" msgstr "" +msgid "Import from Jira" +msgstr "" + msgid "Import in progress" msgstr "" @@ -17829,6 +17829,9 @@ msgstr "" msgid "SecurityDashboard|Severity" msgstr "" +msgid "SecurityDashboard|Status" +msgstr "" + msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects." msgstr "" @@ -22684,6 +22687,21 @@ msgstr "" msgid "VulnerabilityManagement|Will not fix or a false-positive" msgstr "" +msgid "VulnerabilityStatusTypes|All" +msgstr "" + +msgid "VulnerabilityStatusTypes|Confirmed" +msgstr "" + +msgid "VulnerabilityStatusTypes|Detected" +msgstr "" + +msgid "VulnerabilityStatusTypes|Dismissed" +msgstr "" + +msgid "VulnerabilityStatusTypes|Resolved" +msgstr "" + msgid "Vulnerability|Class" msgstr "" @@ -24787,9 +24805,6 @@ msgstr "" msgid "severity|None" msgstr "" -msgid "severity|Undefined" -msgstr "" - msgid "severity|Unknown" msgstr "" diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb index 8f516de3322..3684a1bb8d8 100644 --- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb @@ -157,6 +157,14 @@ describe Projects::Settings::CiCdController do subject end + + it 'creates a pipeline', :sidekiq_inline do + project.repository.create_file(user, 'Gemfile', 'Gemfile contents', + message: 'Add Gemfile', + branch_name: 'master') + + expect { subject }.to change { Ci::Pipeline.count }.by(1) + end end end diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js new file mode 100644 index 00000000000..b3b98a64891 --- /dev/null +++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js @@ -0,0 +1,141 @@ +import Vue from 'vue'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { projectData, branches } from 'jest/ide/mock_data'; +import { createStore } from '~/ide/stores'; +import commitActions from '~/ide/components/commit_sidebar/actions.vue'; +import consts from '~/ide/stores/modules/commit/constants'; + +const ACTION_UPDATE_COMMIT_ACTION = 'commit/updateCommitAction'; + +const BRANCH_DEFAULT = 'master'; +const BRANCH_PROTECTED = 'protected/access'; +const BRANCH_PROTECTED_NO_ACCESS = 'protected/no-access'; +const BRANCH_REGULAR = 'regular'; +const BRANCH_REGULAR_NO_ACCESS = 'regular/no-access'; + +describe('IDE commit sidebar actions', () => { + let store; + let vm; + + const createComponent = ({ hasMR = false, currentBranchId = 'master' } = {}) => { + const Component = Vue.extend(commitActions); + + vm = createComponentWithStore(Component, store); + + vm.$store.state.currentBranchId = currentBranchId; + vm.$store.state.currentProjectId = 'abcproject'; + + const proj = { ...projectData }; + proj.branches[currentBranchId] = branches.find(branch => branch.name === currentBranchId); + + Vue.set(vm.$store.state.projects, 'abcproject', proj); + + if (hasMR) { + vm.$store.state.currentMergeRequestId = '1'; + vm.$store.state.projects[store.state.currentProjectId].mergeRequests[ + store.state.currentMergeRequestId + ] = { foo: 'bar' }; + } + + vm.$mount(); + + return vm; + }; + + beforeEach(() => { + store = createStore(); + jest.spyOn(store, 'dispatch').mockImplementation(() => {}); + }); + + afterEach(() => { + vm.$destroy(); + vm = null; + }); + + it('renders 2 groups', () => { + createComponent(); + + expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2); + }); + + it('renders current branch text', () => { + createComponent(); + + expect(vm.$el.textContent).toContain('Commit to master branch'); + }); + + it('hides merge request option when project merge requests are disabled', done => { + createComponent({ mergeRequestsEnabled: false }); + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2); + expect(vm.$el.textContent).not.toContain('Create a new branch and merge request'); + + done(); + }); + }); + + describe('commitToCurrentBranchText', () => { + it('escapes current branch', () => { + const injectedSrc = '<img src="x" />'; + createComponent({ currentBranchId: injectedSrc }); + + expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc); + }); + }); + + describe('updateSelectedCommitAction', () => { + it('does not return anything if currentBranch does not exist', () => { + createComponent({ currentBranchId: null }); + + expect(vm.$store.dispatch).not.toHaveBeenCalled(); + }); + + it('is not called on mount if there is already a selected commitAction', () => { + store.state.commitAction = '1'; + createComponent({ currentBranchId: null }); + + expect(vm.$store.dispatch).not.toHaveBeenCalled(); + }); + + it('calls again after staged changes', done => { + createComponent({ currentBranchId: null }); + + vm.$store.state.currentBranchId = 'master'; + vm.$store.state.changedFiles.push({}); + vm.$store.state.stagedFiles.push({}); + + vm.$nextTick() + .then(() => { + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + expect.anything(), + ); + }) + .then(done) + .catch(done.fail); + }); + + it.each` + input | expectedOption + ${{ currentBranchId: BRANCH_DEFAULT }} | ${consts.COMMIT_TO_NEW_BRANCH} + ${{ currentBranchId: BRANCH_PROTECTED, hasMR: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH} + ${{ currentBranchId: BRANCH_PROTECTED, hasMR: false }} | ${consts.COMMIT_TO_CURRENT_BRANCH} + ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: true }} | ${consts.COMMIT_TO_NEW_BRANCH} + ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: false }} | ${consts.COMMIT_TO_NEW_BRANCH} + ${{ currentBranchId: BRANCH_REGULAR, hasMR: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH} + ${{ currentBranchId: BRANCH_REGULAR, hasMR: false }} | ${consts.COMMIT_TO_CURRENT_BRANCH} + ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: true }} | ${consts.COMMIT_TO_NEW_BRANCH} + ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: false }} | ${consts.COMMIT_TO_NEW_BRANCH} + `( + 'with $input, it dispatches update commit action with $expectedOption', + ({ input, expectedOption }) => { + createComponent(input); + + expect(vm.$store.dispatch.mock.calls).toEqual([ + [ACTION_UPDATE_COMMIT_ACTION, expectedOption], + ]); + }, + ); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js index 16d0b354a30..16d0b354a30 100644 --- a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js diff --git a/spec/javascripts/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js index f5d1a9de59c..dfde69ab2df 100644 --- a/spec/javascripts/ide/components/commit_sidebar/form_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; -import { projectData } from 'spec/ide/mock_data'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { projectData } from 'jest/ide/mock_data'; import store from '~/ide/stores'; import CommitForm from '~/ide/components/commit_sidebar/form.vue'; import { leftSidebarViews } from '~/ide/constants'; @@ -12,8 +12,6 @@ describe('IDE commit form', () => { let vm; beforeEach(() => { - spyOnProperty(window, 'innerHeight').and.returnValue(800); - store.state.changedFiles.push('test'); store.state.currentProjectId = 'abcproject'; store.state.currentBranchId = 'master'; @@ -111,7 +109,7 @@ describe('IDE commit form', () => { textarea.dispatchEvent(new Event('input')); - getSetTimeoutPromise() + waitForPromises() .then(() => { expect(vm.$store.state.commit.commitMessage).toBe('testing commit message'); }) @@ -148,7 +146,7 @@ describe('IDE commit form', () => { it('resets commitMessage when clicking discard button', done => { vm.$store.state.commit.commitMessage = 'testing commit message'; - getSetTimeoutPromise() + waitForPromises() .then(() => { vm.$el.querySelector('.btn-default').click(); }) @@ -163,14 +161,14 @@ describe('IDE commit form', () => { describe('when submitting', () => { beforeEach(() => { - spyOn(vm, 'commitChanges'); + jest.spyOn(vm, 'commitChanges').mockImplementation(() => {}); vm.$store.state.stagedFiles.push('test'); }); it('calls commitChanges', done => { vm.$store.state.commit.commitMessage = 'testing commit message'; - getSetTimeoutPromise() + waitForPromises() .then(() => { vm.$el.querySelector('.btn-success').click(); }) diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/frontend/ide/components/commit_sidebar/list_collapsed_spec.js index 6eb912127d5..45372d18965 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_collapsed_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import store from '~/ide/stores'; import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue'; import { file } from '../../helpers'; diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js index 63ba6b95619..ebb41448905 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import { trimText } from 'spec/helpers/text_helper'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { trimText } from 'helpers/text_helper'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import store from '~/ide/stores'; import listItem from '~/ide/components/commit_sidebar/list_item.vue'; import router from '~/ide/ide_router'; @@ -61,12 +61,12 @@ describe('Multi-file editor commit sidebar list item', () => { }); it('opens a closed file in the editor when clicking the file path', done => { - spyOn(vm, 'openPendingTab').and.callThrough(); - spyOn(router, 'push'); + jest.spyOn(vm, 'openPendingTab'); + jest.spyOn(router, 'push').mockImplementation(() => {}); findPathEl.click(); - setTimeout(() => { + setImmediate(() => { expect(vm.openPendingTab).toHaveBeenCalled(); expect(router.push).toHaveBeenCalled(); @@ -75,13 +75,13 @@ describe('Multi-file editor commit sidebar list item', () => { }); it('calls updateViewer with diff when clicking file', done => { - spyOn(vm, 'openFileInEditor').and.callThrough(); - spyOn(vm, 'updateViewer').and.callThrough(); - spyOn(router, 'push'); + jest.spyOn(vm, 'openFileInEditor'); + jest.spyOn(vm, 'updateViewer'); + jest.spyOn(router, 'push').mockImplementation(() => {}); findPathEl.click(); - setTimeout(() => { + setImmediate(() => { expect(vm.updateViewer).toHaveBeenCalledWith('diff'); done(); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js index 5a1682523d8..ee209487665 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import store from '~/ide/stores'; import commitSidebarList from '~/ide/components/commit_sidebar/list.vue'; import { file, resetStore } from '../../helpers'; diff --git a/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js index 7c0b4000229..7cbf5ebc61a 100644 --- a/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { projectData, branches } from 'spec/ide/mock_data'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { projectData, branches } from 'jest/ide/mock_data'; import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue'; import { createStore } from '~/ide/stores'; import { PERMISSION_CREATE_MR } from '~/ide/constants'; @@ -200,11 +200,11 @@ describe('create new MR checkbox', () => { currentBranchId: 'regular', }); const el = vm.$el.querySelector('input[type="checkbox"]'); - spyOn(vm.$store, 'dispatch'); + jest.spyOn(vm.$store, 'dispatch').mockImplementation(() => {}); el.dispatchEvent(new Event('change')); - expect(vm.$store.dispatch.calls.allArgs()).toEqual( - jasmine.arrayContaining([['commit/toggleShouldCreateMR', jasmine.any(Object)]]), + expect(vm.$store.dispatch.mock.calls).toEqual( + expect.arrayContaining([['commit/toggleShouldCreateMR', expect.any(Object)]]), ); }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/success_message_spec.js b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js index e1a432b81be..e1a432b81be 100644 --- a/spec/javascripts/ide/components/commit_sidebar/success_message_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js deleted file mode 100644 index a8e6195a67c..00000000000 --- a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js +++ /dev/null @@ -1,235 +0,0 @@ -import Vue from 'vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { projectData, branches } from 'spec/ide/mock_data'; -import { createStore } from '~/ide/stores'; -import commitActions from '~/ide/components/commit_sidebar/actions.vue'; -import consts from '~/ide/stores/modules/commit/constants'; - -const ACTION_UPDATE_COMMIT_ACTION = 'commit/updateCommitAction'; - -describe('IDE commit sidebar actions', () => { - let store; - let vm; - - const createComponent = ({ hasMR = false, currentBranchId = 'master' } = {}) => { - const Component = Vue.extend(commitActions); - - vm = createComponentWithStore(Component, store); - - vm.$store.state.currentBranchId = currentBranchId; - vm.$store.state.currentProjectId = 'abcproject'; - - const proj = { ...projectData }; - proj.branches[currentBranchId] = branches.find(branch => branch.name === currentBranchId); - - Vue.set(vm.$store.state.projects, 'abcproject', proj); - - if (hasMR) { - vm.$store.state.currentMergeRequestId = '1'; - vm.$store.state.projects[store.state.currentProjectId].mergeRequests[ - store.state.currentMergeRequestId - ] = { foo: 'bar' }; - } - - vm.$mount(); - - return vm; - }; - - beforeEach(() => { - store = createStore(); - spyOn(store, 'dispatch'); - }); - - afterEach(() => { - vm.$destroy(); - vm = null; - }); - - it('renders 2 groups', () => { - createComponent(); - - expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2); - }); - - it('renders current branch text', () => { - createComponent(); - - expect(vm.$el.textContent).toContain('Commit to master branch'); - }); - - it('hides merge request option when project merge requests are disabled', done => { - createComponent({ mergeRequestsEnabled: false }); - - vm.$nextTick(() => { - expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2); - expect(vm.$el.textContent).not.toContain('Create a new branch and merge request'); - - done(); - }); - }); - - describe('commitToCurrentBranchText', () => { - it('escapes current branch', () => { - const injectedSrc = '<img src="x" />'; - createComponent({ currentBranchId: injectedSrc }); - - expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc); - }); - }); - - describe('updateSelectedCommitAction', () => { - it('does not return anything if currentBranch does not exist', () => { - createComponent({ currentBranchId: null }); - - expect(vm.$store.dispatch).not.toHaveBeenCalled(); - }); - - it('is not called on mount if there is already a selected commitAction', () => { - store.state.commitAction = '1'; - createComponent({ currentBranchId: null }); - - expect(vm.$store.dispatch).not.toHaveBeenCalled(); - }); - - it('calls again after staged changes', done => { - createComponent({ currentBranchId: null }); - - vm.$store.state.currentBranchId = 'master'; - vm.$store.state.changedFiles.push({}); - vm.$store.state.stagedFiles.push({}); - - vm.$nextTick() - .then(() => { - expect(vm.$store.dispatch).toHaveBeenCalledWith( - ACTION_UPDATE_COMMIT_ACTION, - jasmine.anything(), - ); - }) - .then(done) - .catch(done.fail); - }); - - describe('default branch', () => { - it('dispatches correct action for default branch', () => { - createComponent({ - currentBranchId: 'master', - }); - - expect(vm.$store.dispatch).toHaveBeenCalledTimes(1); - expect(vm.$store.dispatch).toHaveBeenCalledWith( - ACTION_UPDATE_COMMIT_ACTION, - consts.COMMIT_TO_NEW_BRANCH, - ); - }); - }); - - describe('protected branch', () => { - describe('with write access', () => { - it('dispatches correct action when MR exists', () => { - createComponent({ - hasMR: true, - currentBranchId: 'protected/access', - }); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - ACTION_UPDATE_COMMIT_ACTION, - consts.COMMIT_TO_CURRENT_BRANCH, - ); - }); - - it('dispatches correct action when MR does not exists', () => { - createComponent({ - hasMR: false, - currentBranchId: 'protected/access', - }); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - ACTION_UPDATE_COMMIT_ACTION, - consts.COMMIT_TO_CURRENT_BRANCH, - ); - }); - }); - - describe('without write access', () => { - it('dispatches correct action when MR exists', () => { - createComponent({ - hasMR: true, - currentBranchId: 'protected/no-access', - }); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - ACTION_UPDATE_COMMIT_ACTION, - consts.COMMIT_TO_NEW_BRANCH, - ); - }); - - it('dispatches correct action when MR does not exists', () => { - createComponent({ - hasMR: false, - currentBranchId: 'protected/no-access', - }); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - ACTION_UPDATE_COMMIT_ACTION, - consts.COMMIT_TO_NEW_BRANCH, - ); - }); - }); - }); - - describe('regular branch', () => { - describe('with write access', () => { - it('dispatches correct action when MR exists', () => { - createComponent({ - hasMR: true, - currentBranchId: 'regular', - }); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - ACTION_UPDATE_COMMIT_ACTION, - consts.COMMIT_TO_CURRENT_BRANCH, - ); - }); - - it('dispatches correct action when MR does not exists', () => { - createComponent({ - hasMR: false, - currentBranchId: 'regular', - }); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - ACTION_UPDATE_COMMIT_ACTION, - consts.COMMIT_TO_CURRENT_BRANCH, - ); - }); - }); - - describe('without write access', () => { - it('dispatches correct action when MR exists', () => { - createComponent({ - hasMR: true, - currentBranchId: 'regular/no-access', - }); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - ACTION_UPDATE_COMMIT_ACTION, - consts.COMMIT_TO_NEW_BRANCH, - ); - }); - - it('dispatches correct action when MR does not exists', () => { - createComponent({ - hasMR: false, - currentBranchId: 'regular/no-access', - }); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - ACTION_UPDATE_COMMIT_ACTION, - consts.COMMIT_TO_NEW_BRANCH, - ); - }); - }); - }); - }); -}); diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 62adba4319e..0b34e887716 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1647,6 +1647,48 @@ module Gitlab it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /is not defined in prior stages/) } end + + context 'when trigger job includes artifact generated by a dependency' do + context 'when dependency is defined in previous stages' do + let(:config) do + { + build1: { stage: 'build', script: 'test' }, + test1: { stage: 'test', trigger: { + include: [{ job: 'build1', artifact: 'generated.yml' }] + } } + } + end + + it { expect { subject }.not_to raise_error } + end + + context 'when dependency is defined in later stages' do + let(:config) do + { + build1: { stage: 'build', script: 'test' }, + test1: { stage: 'test', trigger: { + include: [{ job: 'deploy1', artifact: 'generated.yml' }] + } }, + deploy1: { stage: 'deploy', script: 'test' } + } + end + + it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /is not defined in prior stages/) } + end + + context 'when dependency is not defined' do + let(:config) do + { + build1: { stage: 'build', script: 'test' }, + test1: { stage: 'test', trigger: { + include: [{ job: 'non-existent', artifact: 'generated.yml' }] + } } + } + end + + it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /undefined dependency: non-existent/) } + end + end end describe "Job Needs" do @@ -2052,6 +2094,34 @@ module Gitlab end end + describe 'with trigger:include' do + context 'when artifact and job are specified' do + let(:config) do + YAML.dump({ + build1: { stage: 'build', script: 'test' }, + test1: { stage: 'test', trigger: { + include: [{ artifact: 'generated.yml', job: 'build1' }] + } } + }) + end + + it { expect { subject }.not_to raise_error } + end + + context 'when artifact is specified without job' do + let(:config) do + YAML.dump({ + build1: { stage: 'build', script: 'test' }, + test1: { stage: 'test', trigger: { + include: [{ artifact: 'generated.yml' }] + } } + }) + end + + it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /must specify the job where to fetch the artifact from/) } + end + end + describe "Error handling" do it "fails to parse YAML" do expect do diff --git a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb index 03631a3e941..808ed6ee2fa 100644 --- a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb +++ b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb @@ -14,6 +14,29 @@ describe Gitlab::JiraImport::IssueSerializer do let(:updated_at) { '2020-01-10 20:00:00' } let(:assignee) { double(displayName: 'Solver') } let(:jira_status) { 'new' } + + let(:parent_field) do + { 'key' => 'FOO-2', 'id' => '1050', 'fields' => { 'summary' => 'parent issue FOO' } } + end + let(:issue_type_field) { { 'name' => 'Task' } } + let(:fix_versions_field) { [{ 'name' => '1.0' }, { 'name' => '1.1' }] } + let(:priority_field) { { 'name' => 'Medium' } } + let(:labels_field) { %w(bug backend) } + let(:environment_field) { 'staging' } + let(:duedate_field) { '2020-03-01' } + + let(:fields) do + { + 'parent' => parent_field, + 'issuetype' => issue_type_field, + 'fixVersions' => fix_versions_field, + 'priority' => priority_field, + 'labels' => labels_field, + 'environment' => environment_field, + 'duedate' => duedate_field + } + end + let(:jira_issue) do double( id: '1234', @@ -24,11 +47,15 @@ describe Gitlab::JiraImport::IssueSerializer do updated: updated_at, assignee: assignee, reporter: double(displayName: 'Reporter'), - status: double(statusCategory: { 'key' => jira_status }) + status: double(statusCategory: { 'key' => jira_status }), + fields: fields ) end + let(:params) { { iid: iid } } + subject { described_class.new(project, jira_issue, params).execute } + let(:expected_description) do <<~MD *Created by: Reporter* @@ -36,11 +63,21 @@ describe Gitlab::JiraImport::IssueSerializer do *Assigned to: Solver* basic description + + --- + + **Issue metadata** + + - Issue type: Task + - Priority: Medium + - Labels: bug, backend + - Environment: staging + - Due date: 2020-03-01 + - Parent issue: [FOO-2] parent issue FOO + - Fix versions: 1.0, 1.1 MD end - subject { described_class.new(project, jira_issue, params).execute } - context 'attributes setting' do it 'sets the basic attributes' do expect(subject).to eq( @@ -54,6 +91,54 @@ describe Gitlab::JiraImport::IssueSerializer do author_id: project.creator_id ) end + + context 'when some metadata fields are missing' do + let(:assignee) { nil } + let(:parent_field) { nil } + let(:fix_versions_field) { [] } + let(:labels_field) { [] } + let(:environment_field) { nil } + let(:duedate_field) { '2020-03-01' } + + it 'skips the missing fields' do + expected_description = <<~MD + *Created by: Reporter* + + basic description + + --- + + **Issue metadata** + + - Issue type: Task + - Priority: Medium + - Due date: 2020-03-01 + MD + + expect(subject[:description]).to eq(expected_description.strip) + end + end + + context 'when all metadata fields are missing' do + let(:assignee) { nil } + let(:parent_field) { nil } + let(:issue_type_field) { nil } + let(:fix_versions_field) { [] } + let(:priority_field) { nil } + let(:labels_field) { [] } + let(:environment_field) { nil } + let(:duedate_field) { nil } + + it 'skips the whole metadata secction' do + expected_description = <<~MD + *Created by: Reporter* + + basic description + MD + + expect(subject[:description]).to eq(expected_description.strip) + end + end end context 'with done status' do @@ -64,20 +149,6 @@ describe Gitlab::JiraImport::IssueSerializer do end end - context 'without the assignee' do - let(:assignee) { nil } - - it 'does not include assignee in the description' do - expected_description = <<~MD - *Created by: Reporter* - - basic description - MD - - expect(subject[:description]).to eq(expected_description.strip) - end - end - context 'without the iid' do let(:params) { {} } diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index 5808d6e37e5..81f173cd23a 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -302,20 +302,55 @@ describe Milestone, 'Milestoneish' do end end - describe '#total_issue_time_spent' do - it 'calculates total issue time spent' do + describe '#total_time_spent' do + it 'calculates total time spent' do closed_issue_1.spend_time(duration: 300, user_id: author.id) closed_issue_1.save! closed_issue_2.spend_time(duration: 600, user_id: assignee.id) closed_issue_2.save! - expect(milestone.total_issue_time_spent).to eq(900) + expect(milestone.total_time_spent).to eq(900) + end + + it 'includes merge request time spent' do + closed_issue_1.spend_time(duration: 300, user_id: author.id) + closed_issue_1.save! + merge_request.spend_time(duration: 900, user_id: author.id) + merge_request.save! + + expect(milestone.total_time_spent).to eq(1200) + end + end + + describe '#human_total_time_spent' do + it 'returns nil if no time has been spent' do + expect(milestone.human_total_time_spent).to be_nil + end + end + + describe '#total_time_estimate' do + it 'calculates total estimate' do + closed_issue_1.time_estimate = 300 + closed_issue_1.save! + closed_issue_2.time_estimate = 600 + closed_issue_2.save! + + expect(milestone.total_time_estimate).to eq(900) + end + + it 'includes merge request time estimate' do + closed_issue_1.time_estimate = 300 + closed_issue_1.save! + merge_request.time_estimate = 900 + merge_request.save! + + expect(milestone.total_time_estimate).to eq(1200) end end - describe '#human_total_issue_time_spent' do + describe '#human_total_time_estimate' do it 'returns nil if no time has been spent' do - expect(milestone.human_total_issue_time_spent).to be_nil + expect(milestone.human_total_time_estimate).to be_nil end end end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 93b2c19c74a..10cf76b607f 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -40,10 +40,6 @@ describe Admin::UsersController, "routing" do expect(get("/admin/users/1/edit")).to route_to('admin/users#edit', id: '1') end - it "to #show" do - expect(get("/admin/users/1")).to route_to('admin/users#show', id: '1') - end - it "to #update" do expect(put("/admin/users/1")).to route_to('admin/users#update', id: '1') end diff --git a/spec/services/ci/daily_report_result_service_spec.rb b/spec/services/ci/daily_report_result_service_spec.rb index 793fc956acb..240709bab0b 100644 --- a/spec/services/ci/daily_report_result_service_spec.rb +++ b/spec/services/ci/daily_report_result_service_spec.rb @@ -38,6 +38,27 @@ describe Ci::DailyReportResultService, '#execute' do expect(Ci::DailyReportResult.find_by(title: 'extra')).to be_nil end + context 'when there are multiple builds with the same group name that report coverage' do + let!(:test_job_1) { create(:ci_build, pipeline: pipeline, name: '1/2 test', coverage: 70) } + let!(:test_job_2) { create(:ci_build, pipeline: pipeline, name: '2/2 test', coverage: 80) } + + it 'creates daily code coverage record with the average as the value' do + described_class.new.execute(pipeline) + + Ci::DailyReportResult.find_by(title: 'test').tap do |coverage| + expect(coverage).to have_attributes( + project_id: pipeline.project.id, + last_pipeline_id: pipeline.id, + ref_path: pipeline.source_ref_path, + param_type: 'coverage', + title: test_job_2.group_name, + value: 75, + date: pipeline.created_at.to_date + ) + end + end + end + context 'when there is an existing daily code coverage for the matching date, project, ref_path, and group name' do let!(:new_pipeline) do create( |