diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-01 09:07:45 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-01 09:07:45 +0000 |
commit | b11f7057d067885619ee3e513751f180b2e8ad85 (patch) | |
tree | dfb3077ea8716ed217f5ce4324be4e25a450c599 | |
parent | e50050a8756a20b6aa118edbad3369674e4c63ba (diff) | |
download | gitlab-ce-b11f7057d067885619ee3e513751f180b2e8ad85.tar.gz |
Add latest changes from gitlab-org/gitlab@master
43 files changed, 538 insertions, 214 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index c083116cc27..334eb4b1430 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -499,6 +499,7 @@ .review:rules:review-gcp-cleanup: rules: - <<: *if-dot-com-gitlab-org-merge-request + changes: *code-qa-patterns when: manual - <<: *if-dot-com-gitlab-org-schedule when: on_success diff --git a/.rubocop.yml b/.rubocop.yml index 2544675eea3..5e2cde935b2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -422,7 +422,6 @@ RSpec/RepeatedExample: - 'spec/services/notification_service_spec.rb' - 'spec/services/web_hook_service_spec.rb' - 'ee/spec/models/group_spec.rb' - - 'ee/spec/models/user_spec.rb' - 'ee/spec/requests/api/merge_request_approvals_spec.rb' - 'ee/spec/services/boards/lists/update_service_spec.rb' - 'ee/spec/services/geo/repository_verification_primary_service_spec.rb' diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index d4270960f57..c51f711fb81 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -172,7 +172,6 @@ export default { /> <a v-once - id="diffFile.file_path" ref="titleWrapper" class="append-right-4" :href="titleLink" diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue index af1574f98d9..624ca18eec9 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue @@ -7,21 +7,18 @@ export default { BlobHeaderEdit, BlobContentEdit, }, + inheritAttrs: false, props: { - content: { - type: String, - required: true, - }, fileName: { type: String, - required: true, + required: false, + default: '', }, }, - data() { - return { - name: this.fileName, - blobContent: this.content, - }; + methods: { + emitFileNameChange(newFileName) { + this.$emit('name-change', newFileName); + }, }, }; </script> @@ -29,8 +26,8 @@ export default { <div class="form-group file-editor"> <label>{{ s__('Snippets|File') }}</label> <div class="file-holder snippet"> - <blob-header-edit v-model="name" /> - <blob-content-edit v-model="blobContent" :file-name="name" /> + <blob-header-edit :value="fileName" @input="emitFileNameChange" /> + <blob-content-edit v-bind="$attrs" :file-name="fileName" v-on="$listeners" /> </div> </div> </template> diff --git a/app/assets/javascripts/snippets/components/snippet_description_edit.vue b/app/assets/javascripts/snippets/components/snippet_description_edit.vue index 5b70ac5b715..68810f8ab3f 100644 --- a/app/assets/javascripts/snippets/components/snippet_description_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_description_edit.vue @@ -9,11 +9,6 @@ export default { MarkdownField, }, props: { - description: { - type: String, - default: '', - required: false, - }, markdownPreviewPath: { type: String, required: true, @@ -22,11 +17,11 @@ export default { type: String, required: true, }, - }, - data() { - return { - text: this.description, - }; + value: { + type: String, + required: false, + default: '', + }, }, mounted() { setupCollapsibleInputs(); @@ -37,7 +32,7 @@ export default { <div class="form-group js-description-input"> <label>{{ s__('Snippets|Description (optional)') }}</label> <div class="js-collapsible-input"> - <div class="js-collapsed" :class="{ 'd-none': text }"> + <div class="js-collapsed" :class="{ 'd-none': value }"> <gl-form-input class="form-control" :placeholder=" @@ -50,20 +45,21 @@ export default { </div> <markdown-field class="js-expanded" - :class="{ 'd-none': !text }" + :class="{ 'd-none': !value }" :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" > <textarea id="snippet-description" slot="textarea" - v-model="text" class="note-textarea js-gfm-input js-autosize markdown-area qa-description-textarea" dir="auto" data-supports-quick-actions="false" + :value="value" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files here…')" + @input="$emit('input', $event.target.value)" > </textarea> </markdown-field> diff --git a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue index 93cd2b58c11..80710a88bb2 100644 --- a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue @@ -1,6 +1,6 @@ <script> import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui'; -import { SNIPPET_VISIBILITY } from '~/snippets/constants'; +import { SNIPPET_VISIBILITY, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants'; export default { components: { @@ -21,48 +21,22 @@ export default { required: false, default: false, }, - visibilityLevel: { + value: { type: String, - default: '0', required: false, + default: SNIPPET_VISIBILITY_PRIVATE, }, }, - data() { - return { - selected: this.visibilityLevel, - }; - }, computed: { visibilityOptions() { - return [ - { - value: '0', - icon: 'lock', - text: SNIPPET_VISIBILITY.private.label, - description: this.isProjectSnippet - ? SNIPPET_VISIBILITY.private.description_project - : SNIPPET_VISIBILITY.private.description, - }, - { - value: '1', - icon: 'shield', - text: SNIPPET_VISIBILITY.internal.label, - description: SNIPPET_VISIBILITY.internal.description, - }, - { - value: '2', - icon: 'earth', - text: SNIPPET_VISIBILITY.public.label, - description: SNIPPET_VISIBILITY.public.description, - }, - ]; - }, - }, - methods: { - updateSelectedOption(newVal) { - if (newVal !== this.selected) { - this.selected = newVal; - } + const options = []; + Object.keys(SNIPPET_VISIBILITY).forEach(key => { + options.push({ + value: key, + ...SNIPPET_VISIBILITY[key], + }); + }); + return options; }, }, }; @@ -76,18 +50,22 @@ export default { /></gl-link> </label> <gl-form-group id="visibility-level-setting"> - <gl-form-radio-group :checked="selected" stacked @change="updateSelectedOption"> + <gl-form-radio-group v-bind="$attrs" :checked="value" stacked v-on="$listeners"> <gl-form-radio v-for="option in visibilityOptions" - :key="option.icon" + :key="option.value" :value="option.value" class="mb-3" > <div class="d-flex align-items-center"> <gl-icon :size="16" :name="option.icon" /> - <span class="font-weight-bold ml-1">{{ option.text }}</span> + <span class="font-weight-bold ml-1 js-visibility-option">{{ option.label }}</span> </div> - <template #help>{{ option.description }}</template> + <template #help>{{ + isProjectSnippet && option.description_project + ? option.description_project + : option.description + }}</template> </gl-form-radio> </gl-form-radio-group> </gl-form-group> diff --git a/app/assets/javascripts/snippets/constants.js b/app/assets/javascripts/snippets/constants.js index ed2f1156292..7fd5e5b8ee4 100644 --- a/app/assets/javascripts/snippets/constants.js +++ b/app/assets/javascripts/snippets/constants.js @@ -5,17 +5,20 @@ export const SNIPPET_VISIBILITY_INTERNAL = 'internal'; export const SNIPPET_VISIBILITY_PUBLIC = 'public'; export const SNIPPET_VISIBILITY = { - private: { + [SNIPPET_VISIBILITY_PRIVATE]: { label: __('Private'), + icon: 'lock', description: __('The snippet is visible only to me.'), description_project: __('The snippet is visible only to project members.'), }, - internal: { + [SNIPPET_VISIBILITY_INTERNAL]: { label: __('Internal'), + icon: 'shield', description: __('The snippet is visible to any logged in user.'), }, - public: { + [SNIPPET_VISIBILITY_PUBLIC]: { label: __('Public'), + icon: 'earth', description: __('The snippet can be accessed without any authentication.'), }, }; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 43c7c03fa58..57afe45a74b 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -281,7 +281,7 @@ table { display: table; svg { - fill: $gray-darkest; + fill: $gray-700; } .btn-group { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 25d8fc1f9a3..8b51ba7ae62 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -855,7 +855,7 @@ $note-form-margin-left: 72px; line-height: $gl-line-height; svg { - fill: $gray-darkest; + fill: $gray-700; } &.discussion-create-issue-btn { @@ -893,7 +893,7 @@ $note-form-margin-left: 72px; .line-resolve-btn { margin-right: 5px; - color: $gray-darkest; + color: $gray-700; svg { vertical-align: middle; diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 961694bd91f..2c3611875a2 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -21,6 +21,8 @@ # non_archived: boolean # archived: 'only' or boolean # min_access_level: integer +# last_activity_after: datetime +# last_activity_before: datetime # class ProjectsFinder < UnionFinder include CustomAttributesFilter @@ -73,6 +75,8 @@ class ProjectsFinder < UnionFinder collection = by_archived(collection) collection = by_custom_attributes(collection) collection = by_deleted_status(collection) + collection = by_last_activity_after(collection) + collection = by_last_activity_before(collection) collection end @@ -179,6 +183,22 @@ class ProjectsFinder < UnionFinder params[:without_deleted].present? ? items.without_deleted : items end + def by_last_activity_after(items) + if params[:last_activity_after].present? + items.where("last_activity_at > ?", params[:last_activity_after]) # rubocop: disable CodeReuse/ActiveRecord + else + items + end + end + + def by_last_activity_before(items) + if params[:last_activity_before].present? + items.where("last_activity_at < ?", params[:last_activity_before]) # rubocop: disable CodeReuse/ActiveRecord + else + items + end + end + def sort(items) params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.projects_order_id_desc end diff --git a/app/models/label.rb b/app/models/label.rb index d9c5fe0bb39..652b5e23490 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -159,6 +159,11 @@ class Label < ApplicationRecord on_project_boards(project_id).where(id: label_id).exists? end + # Generate a hex color based on hex-encoded value + def self.color_for(value) + "##{Digest::MD5.hexdigest(value)[0..5]}" + end + def open_issues_count(user = nil) issues_count(user, state: 'opened') end diff --git a/app/workers/gitlab/jira_import/import_issue_worker.rb b/app/workers/gitlab/jira_import/import_issue_worker.rb index 832916a03b6..7ace0a35fd9 100644 --- a/app/workers/gitlab/jira_import/import_issue_worker.rb +++ b/app/workers/gitlab/jira_import/import_issue_worker.rb @@ -9,8 +9,8 @@ module Gitlab include Gitlab::Import::DatabaseHelpers def perform(project_id, jira_issue_id, issue_attributes, waiter_key) - issue_id = insert_and_return_id(issue_attributes, Issue) - cache_issue_mapping(issue_id, jira_issue_id, project_id) + issue_id = create_issue(issue_attributes, project_id) + JiraImport.cache_issue_mapping(issue_id, jira_issue_id, project_id) rescue => ex # Todo: Record jira issue id(or better jira issue key), # so that we can report the list of failed to import issues to the user @@ -27,9 +27,31 @@ module Gitlab private - def cache_issue_mapping(issue_id, jira_issue_id, project_id) - cache_key = JiraImport.jira_issue_cache_key(project_id, jira_issue_id) - Gitlab::Cache::Import::Caching.write(cache_key, issue_id) + def create_issue(issue_attributes, project_id) + issue_id = insert_and_return_id(issue_attributes, Issue) + + label_issue(project_id, issue_id) + + issue_id + end + + def label_issue(project_id, issue_id) + label_id = JiraImport.get_import_label_id(project_id) + return unless label_id + + label_link_attrs = build_label_attrs(issue_id, label_id.to_i) + insert_and_return_id(label_link_attrs, LabelLink) + end + + def build_label_attrs(issue_id, label_id) + time = Time.now + { + label_id: label_id, + target_id: issue_id, + target_type: 'Issue', + created_at: time, + updated_at: time + } end end end diff --git a/app/workers/gitlab/jira_import/stage/import_labels_worker.rb b/app/workers/gitlab/jira_import/stage/import_labels_worker.rb index b96bb1bbdda..0499749198a 100644 --- a/app/workers/gitlab/jira_import/stage/import_labels_worker.rb +++ b/app/workers/gitlab/jira_import/stage/import_labels_worker.rb @@ -9,10 +9,8 @@ module Gitlab private def import(project) - # fake labels import workers for now - # new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage - fake_waiter = JobWaiter.new - Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { fake_waiter.key => fake_waiter.jobs_remaining }, :issues) + job_waiter = Gitlab::JiraImport::LabelsImporter.new(project).execute + Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { job_waiter.key => job_waiter.jobs_remaining }, :issues) end end end diff --git a/changelogs/unreleased/208502-add-app-server-type-to-usage-ping.yml b/changelogs/unreleased/208502-add-app-server-type-to-usage-ping.yml new file mode 100644 index 00000000000..65fb93217e8 --- /dev/null +++ b/changelogs/unreleased/208502-add-app-server-type-to-usage-ping.yml @@ -0,0 +1,5 @@ +--- +title: Add app server type to usage ping +merge_request: 28189 +author: +type: added diff --git a/changelogs/unreleased/feat-api-project-last-activity.yml b/changelogs/unreleased/feat-api-project-last-activity.yml new file mode 100644 index 00000000000..3a1f6bd0249 --- /dev/null +++ b/changelogs/unreleased/feat-api-project-last-activity.yml @@ -0,0 +1,5 @@ +--- +title: Add last_activity_before and last_activity_after filter to /api/projects endpoint +merge_request: 28221 +author: Roger Meier +type: added diff --git a/changelogs/unreleased/pedroms-fix-mr-threads-icon-button-color.yml b/changelogs/unreleased/pedroms-fix-mr-threads-icon-button-color.yml new file mode 100644 index 00000000000..c1cb4844eea --- /dev/null +++ b/changelogs/unreleased/pedroms-fix-mr-threads-icon-button-color.yml @@ -0,0 +1,5 @@ +--- +title: Fix merge request thread’s icon buttons color +merge_request: 28465 +author: +type: other diff --git a/doc/api/projects.md b/doc/api/projects.md index 04775b0339d..952d39af8f4 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -41,26 +41,28 @@ GET /projects | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `archived` | boolean | no | Limit by archived status | -| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` | -| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | -| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | -| `search` | string | no | Return list of projects matching the search criteria | -| `search_namespaces` | boolean | no | Include ancestor namespaces when matching search criteria. Default is `false` | -| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. | -| `owned` | boolean | no | Limit by projects explicitly owned by the current user | -| `membership` | boolean | no | Limit by projects that the current user is a member of | -| `starred` | boolean | no | Limit by projects starred by the current user | -| `statistics` | boolean | no | Include project statistics | -| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | -| `with_issues_enabled` | boolean | no | Limit by enabled issues feature | -| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature | -| `with_programming_language` | string | no | Limit by projects which use the given programming language | -| `wiki_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the wiki checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) | -| `repository_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the repository checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) | -| `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) | -| `id_after` | integer | no | Limit results to projects with IDs greater than the specified ID | -| `id_before` | integer | no | Limit results to projects with IDs less than the specified ID | +| `archived` | boolean | no | Limit by archived status | +| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` | +| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | +| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | +| `search` | string | no | Return list of projects matching the search criteria | +| `search_namespaces` | boolean | no | Include ancestor namespaces when matching search criteria. Default is `false` | +| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. | +| `owned` | boolean | no | Limit by projects explicitly owned by the current user | +| `membership` | boolean | no | Limit by projects that the current user is a member of | +| `starred` | boolean | no | Limit by projects starred by the current user | +| `statistics` | boolean | no | Include project statistics | +| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | +| `with_issues_enabled` | boolean | no | Limit by enabled issues feature | +| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature | +| `with_programming_language` | string | no | Limit by projects which use the given programming language | +| `wiki_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the wiki checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) | +| `repository_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the repository checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) | +| `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) | +| `id_after` | integer | no | Limit results to projects with IDs greater than the specified ID | +| `id_before` | integer | no | Limit results to projects with IDs less than the specified ID | +| `last_activity_after` | datetime | no | Limit results to projects with last_activity after specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ | +| `last_activity_before` | datetime | no | Limit results to projects with last_activity before specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ | NOTE: **Note:** This endpoint supports [keyset pagination](README.md#keyset-based-pagination) for selected `order_by` options. diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md index 8807e1beab8..4797251129b 100644 --- a/doc/ci/git_submodules.md +++ b/doc/ci/git_submodules.md @@ -6,7 +6,7 @@ type: reference > **Notes:** > -> - GitLab 8.12 introduced a new [CI job permissions model][newperms] and you +> - GitLab 8.12 introduced a new [CI job permissions model](../user/project/new_ci_build_permissions_model.md) and you > are encouraged to upgrade your GitLab instance if you haven't done already. > If you are **not** using GitLab 8.12 or higher, you would need to work your way > around submodules in order to access the sources of e.g., `gitlab.com/group/project` diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md index 90fd44bdf24..78c9965aa08 100644 --- a/doc/ci/junit_test_reports.md +++ b/doc/ci/junit_test_reports.md @@ -224,7 +224,7 @@ with failed showing at the top, skipped next and successful cases last. This feature comes with the `:junit_pipeline_view` feature flag disabled by default. This feature is disabled due to some performance issues with very large data sets. -When [the performance issue](https://gitlab.com/gitlab-org/gitlab/issues/37725) is resolved, the feature will be enabled by default. +When [the performance is improved](https://gitlab.com/groups/gitlab-org/-/epics/2854), the feature will be enabled by default. To enable this feature, ask a GitLab administrator with Rails console access to run the following command: diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md index 09fb5e3a7b9..7c79bf350b9 100644 --- a/doc/ci/multi_project_pipelines.md +++ b/doc/ci/multi_project_pipelines.md @@ -51,7 +51,7 @@ and when hovering or tapping (on touchscreen devices) they will expand and be sh ## Triggering multi-project pipelines through API -> - Use of `CI_JOB_TOKEN` for multi-project pipelines was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2017) in [GitLab Premium][ee] 9.3. +> - Use of `CI_JOB_TOKEN` for multi-project pipelines was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2017) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.3. > - Use of `CI_JOB_TOKEN` for multi-project pipelines was [made available](https://gitlab.com/gitlab-org/gitlab/issues/31573) in all tiers in GitLab 12.4. When you use the [`CI_JOB_TOKEN` to trigger pipelines](triggers/README.md#ci-job-token), GitLab diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index 4f1c266a9bf..b231fa45f6d 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -362,7 +362,7 @@ Check specific punctuation rules for [lists](#lists) below. | Rule | Example | | ---- | ------- | | Always end full sentences with a period. | _For a complete overview, read through this document._| -| Always add a space after a period when beginning a new sentence | _For a complete overview, check this doc. For other references, check out this guide._ | +| Always add a space after a period when beginning a new sentence. | _For a complete overview, check this doc. For other references, check out this guide._ | | Do not use double spaces. | --- | | Do not use tabs for indentation. Use spaces instead. You can configure your code editor to output spaces instead of tabs when pressing the tab key. | --- | | Use serial commas ("Oxford commas") before the final 'and/or' in a list. | _You can create new issues, merge requests, and milestones._ | @@ -816,7 +816,7 @@ you have your MR reviewed and approved by a technical writer. 1. Copy the code below and paste it into your Markdown file. Leave a blank line above and below it. Do NOT edit the code - (don't remove or add any spaces, etc). + (don't remove or add any spaces). 1. On YouTube, visit the video URL you want to display. Copy the regular URL from your browser (`https://www.youtube.com/watch?v=VIDEO-ID`) and replace the video title and link in the line under `<div class="video-fallback">`. @@ -1000,7 +1000,7 @@ Whenever you need to call special attention to particular sentences, use the following markup for highlighting. _Note that the alert boxes only work for one paragraph only. Multiple paragraphs, -lists, headers, etc will not render correctly. For multiple lines, use blockquotes instead._ +lists, headers and so on, will not render correctly. For multiple lines, use blockquotes instead._ Alert boxes only render on the GitLab Docs site (<https://docs.gitlab.com>). Within GitLab itself, they will appear as plain Markdown text (like the examples diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index 6314eaad35f..47497e7b385 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -427,14 +427,14 @@ There are several Rake tasks available to you via the command line: - [`sudo gitlab-rake gitlab:elastic:projects_not_indexed`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) - Displays which projects are not indexed. - [`sudo gitlab-rake gitlab:elastic:reindex_to_another_cluster[<SOURCE_CLUSTER_URL>,<DESTINATION_CLUSTER_URL>]`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) - - Creates a new index in the destination cluster and triggers a [reindex from - remote](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote) - such that the index is fully copied from the source index. This can be - useful when you wish to perform a migration to a new cluster as this - reindexing should be quicker than reindexing via GitLab. Note that remote - reindex requires your source cluster to be whitelisted in your destination - cluster in Elasticsearch settings as per [the - documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote). + - Creates a new index in the destination cluster from the source index using + Elasticsearch "reindex from remote", where the source index is copied to the + destination. This is useful when migrating to a new cluster because it should be + quicker than reindexing via GitLab. + + NOTE: **Note:** + Your source cluster must be whitelisted in your destination cluster's Elasticsearch + settings. See [Reindex from remote](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote). ### Environment Variables diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 5eb23326e43..bf58a2f7510 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -1036,8 +1036,8 @@ Then add any extra changes you want. Your additions will be merged with the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml) using the behaviour described for [`include`](../../ci/yaml/README.md#include). -It is also possible to copy and paste the contents of the [Auto DevOps -template] into your project and edit this as needed. You may prefer to do it +It is also possible to copy and paste the contents of the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml) +into your project and edit this as needed. You may prefer to do it that way if you want to specifically remove any part of it. ### Customizing the Kubernetes namespace diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 4b443db7b54..9f24807ddf4 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -20,7 +20,7 @@ comment at any time, and anyone with [Maintainer access level](../permissions.md higher can also edit a comment made by someone else. You can also reply to a comment notification email to reply to the comment if -[Reply by email] is configured for your GitLab instance. Replying to a standard comment +[Reply by email](../../administration/reply_by_email.md) is configured for your GitLab instance. Replying to a standard comment creates another standard comment. Replying to a threaded comment creates a reply in the thread. Email replies support [Markdown](../markdown.md) and [quick actions](../project/quick_actions.md), just as if you replied from the web. @@ -140,7 +140,7 @@ You can now proceed to merge the merge request from the UI. ### Moving a single thread to a new issue -> [Introduced][ce-8266] in GitLab 9.1 +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8266) in GitLab 9.1 To create a new issue for a single thread, you can use the **Resolve this thread in a new issue** button. diff --git a/doc/user/project/canary_deployments.md b/doc/user/project/canary_deployments.md index 26cf58654f2..f12a281584f 100644 --- a/doc/user/project/canary_deployments.md +++ b/doc/user/project/canary_deployments.md @@ -35,7 +35,7 @@ to be careful when using canaries with user-facing changes, because by default, requests from the same user will be randomly distributed between canary and non-canary pods, which could result in confusion or even errors. If needed, you may want to consider [setting `service.spec.sessionAffinity` to `ClientIP` in -your Kubernetes service definitions][kube-net](https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies), but that is beyond the scope of +your Kubernetes service definitions](https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies), but that is beyond the scope of this document. ## Enabling Canary Deployments diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index b961478ea6d..7968f5d1463 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -459,7 +459,7 @@ Note the following properties: | Property | Type | Required | Description | | ------ | ------ | ------ | ------ | | `type` | string | yes | Type of panel to be rendered. For bar chart types, set to `bar` | -| `query_range` | yes | yes | For bar chart, you must use a [range query] +| `query_range` | yes | yes | For bar chart, you must use a [range query](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) ![bar chart panel type](img/prometheus_dashboard_bar_chart_panel_type_v12.10.png) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 2f603dd6bed..be41788ac77 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -505,20 +505,28 @@ module API protected - def project_finder_params_ce - finder_params = { without_deleted: true } + def project_finder_params_visibility_ce + finder_params = {} + finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level] + finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility] finder_params[:owned] = true if params[:owned].present? finder_params[:non_public] = true if params[:membership].present? finder_params[:starred] = true if params[:starred].present? - finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility] finder_params[:archived] = archived_param unless params[:archived].nil? + finder_params + end + + def project_finder_params_ce + finder_params = project_finder_params_visibility_ce + finder_params[:without_deleted] = true finder_params[:search] = params[:search] if params[:search] finder_params[:search_namespaces] = true if params[:search_namespaces].present? finder_params[:user] = params.delete(:user) if params[:user] finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes] - finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level] finder_params[:id_after] = params[:id_after] if params[:id_after] finder_params[:id_before] = params[:id_before] if params[:id_before] + finder_params[:last_activity_after] = params[:last_activity_after] if params[:last_activity_after] + finder_params[:last_activity_before] = params[:last_activity_before] if params[:last_activity_before] finder_params end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index a33418c3336..ee0731a331f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -73,6 +73,8 @@ module API optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user' optional :id_after, type: Integer, desc: 'Limit results to projects with IDs greater than the specified ID' optional :id_before, type: Integer, desc: 'Limit results to projects with IDs less than the specified ID' + optional :last_activity_after, type: DateTime, desc: 'Limit results to projects with last_activity after specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ' + optional :last_activity_before, type: DateTime, desc: 'Limit results to projects with last_activity before specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ' use :optional_filter_params_ee end diff --git a/lib/gitlab/jira_import.rb b/lib/gitlab/jira_import.rb index 1486c754caf..fe4351d9029 100644 --- a/lib/gitlab/jira_import.rb +++ b/lib/gitlab/jira_import.rb @@ -6,6 +6,7 @@ module Gitlab FAILED_ISSUES_COUNTER_KEY = 'jira-import/failed/%{project_id}/%{collection_type}' NEXT_ITEMS_START_AT_KEY = 'jira-import/paginator/%{project_id}/%{collection_type}' + JIRA_IMPORT_LABEL = 'jira-import/import-label/%{project_id}' ITEMS_MAPPER_CACHE_KEY = 'jira-import/items-mapper/%{project_id}/%{collection_type}/%{jira_isssue_id}' ALREADY_IMPORTED_ITEMS_CACHE_KEY = 'jira-importer/already-imported/%{project}/%{collection_type}' @@ -25,23 +26,45 @@ module Gitlab FAILED_ISSUES_COUNTER_KEY % { project_id: project_id, collection_type: :issues } end + def self.import_label_cache_key(project_id) + JIRA_IMPORT_LABEL % { project_id: project_id } + end + def self.increment_issue_failures(project_id) - Gitlab::Cache::Import::Caching.increment(self.failed_issues_counter_cache_key(project_id)) + cache_class.increment(self.failed_issues_counter_cache_key(project_id)) end def self.get_issues_next_start_at(project_id) - Gitlab::Cache::Import::Caching.read(self.jira_issues_next_page_cache_key(project_id)).to_i + cache_class.read(self.jira_issues_next_page_cache_key(project_id)).to_i end def self.store_issues_next_started_at(project_id, value) cache_key = self.jira_issues_next_page_cache_key(project_id) - Gitlab::Cache::Import::Caching.write(cache_key, value) + cache_class.write(cache_key, value) + end + + def self.cache_issue_mapping(issue_id, jira_issue_id, project_id) + cache_key = JiraImport.jira_issue_cache_key(project_id, jira_issue_id) + cache_class.write(cache_key, issue_id) + end + + def self.get_import_label_id(project_id) + cache_class.read(JiraImport.import_label_cache_key(project_id)) + end + + def self.cache_import_label_id(project_id, label_id) + cache_class.write(JiraImport.import_label_cache_key(project_id), label_id) end def self.cache_cleanup(project_id) - Gitlab::Cache::Import::Caching.expire(self.failed_issues_counter_cache_key(project_id), JIRA_IMPORT_CACHE_TIMEOUT) - Gitlab::Cache::Import::Caching.expire(self.jira_issues_next_page_cache_key(project_id), JIRA_IMPORT_CACHE_TIMEOUT) - Gitlab::Cache::Import::Caching.expire(self.already_imported_cache_key(:issues, project_id), JIRA_IMPORT_CACHE_TIMEOUT) + cache_class.expire(self.import_label_cache_key(project_id), JIRA_IMPORT_CACHE_TIMEOUT) + cache_class.expire(self.failed_issues_counter_cache_key(project_id), JIRA_IMPORT_CACHE_TIMEOUT) + cache_class.expire(self.jira_issues_next_page_cache_key(project_id), JIRA_IMPORT_CACHE_TIMEOUT) + cache_class.expire(self.already_imported_cache_key(:issues, project_id), JIRA_IMPORT_CACHE_TIMEOUT) + end + + def self.cache_class + Gitlab::Cache::Import::Caching end end end diff --git a/lib/gitlab/jira_import/labels_importer.rb b/lib/gitlab/jira_import/labels_importer.rb new file mode 100644 index 00000000000..142a2da5be9 --- /dev/null +++ b/lib/gitlab/jira_import/labels_importer.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab + module JiraImport + class LabelsImporter < BaseImporter + attr_reader :job_waiter + + def initialize(project) + super + @job_waiter = JobWaiter.new + end + + def execute + create_import_label(project) + import_jira_labels + end + + private + + def create_import_label(project) + label = Labels::CreateService.new(build_label_attrs(project)).execute(project: project) + raise Projects::ImportService::Error, _('Failed to create import label for jira import.') unless label + + JiraImport.cache_import_label_id(project.id, label.id) + end + + def build_label_attrs(project) + import_start_time = project&.import_state&.last_update_started_at || Time.now + title = "jira-import-#{import_start_time.strftime('%Y-%m-%d-%H-%M-%S')}" + description = "Label for issues that were imported from jira on #{import_start_time.strftime('%Y-%m-%d %H:%M:%S')}" + color = "#{Label.color_for(title)}" + + { title: title, description: description, color: color } + end + + def import_jira_labels + # todo: import jira labels, see https://gitlab.com/gitlab-org/gitlab/-/issues/212651 + job_waiter + end + end + end +end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index c131566380e..733cafd9d6c 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -174,10 +174,19 @@ module Gitlab git: { version: Gitlab::Git.version }, gitaly: { version: Gitaly::Server.all.first.server_version, servers: Gitaly::Server.count, filesystems: Gitaly::Server.filesystems }, gitlab_pages: { enabled: Gitlab.config.pages.enabled, version: Gitlab::Pages::VERSION }, - database: { adapter: Gitlab::Database.adapter_name, version: Gitlab::Database.version } + database: { adapter: Gitlab::Database.adapter_name, version: Gitlab::Database.version }, + app_server: { type: app_server_type } } end + def app_server_type + Gitlab::Runtime.identify.to_s + rescue Gitlab::Runtime::IdentificationError => e + Gitlab::AppLogger.error(e.message) + Gitlab::ErrorTracking.track_exception(e) + 'unknown_app_server_type' + end + def ingress_modsecurity_usage ::Clusters::Applications::IngressModsecurityUsageService.new.execute end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 41baa2946aa..4484260b70e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6901,9 +6901,6 @@ msgstr "" msgid "Detect host keys" msgstr "" -msgid "Detected %{timeago} in pipeline %{pipelineLink}" -msgstr "" - msgid "DevOps Score" msgstr "" @@ -8481,6 +8478,9 @@ msgstr "" msgid "Failed to create a branch for this issue. Please try again." msgstr "" +msgid "Failed to create import label for jira import." +msgstr "" + msgid "Failed to create repository" msgstr "" @@ -22577,18 +22577,33 @@ msgstr "" msgid "VulnerabilityManagement|Confirm" msgstr "" +msgid "VulnerabilityManagement|Confirmed %{timeago} by %{user}" +msgstr "" + msgid "VulnerabilityManagement|Create issue" msgstr "" +msgid "VulnerabilityManagement|Detected %{timeago} in pipeline %{pipelineLink}" +msgstr "" + msgid "VulnerabilityManagement|Dismiss" msgstr "" +msgid "VulnerabilityManagement|Dismissed %{timeago} by %{user}" +msgstr "" + msgid "VulnerabilityManagement|Resolved" msgstr "" +msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}" +msgstr "" + msgid "VulnerabilityManagement|Something went wrong, could not create an issue." msgstr "" +msgid "VulnerabilityManagement|Something went wrong, could not get user." +msgstr "" + msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state." msgstr "" diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index eb3e28d1668..626517f8fa0 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -222,6 +222,28 @@ describe ProjectsFinder, :do_not_mock_admin_mode do it { is_expected.to match_array([public_project, internal_project]) } end + describe 'filter by last_activity_after' do + let(:params) { { last_activity_after: 60.minutes.ago } } + + before do + internal_project.update(last_activity_at: Time.now) + public_project.update(last_activity_at: 61.minutes.ago) + end + + it { is_expected.to match_array([internal_project]) } + end + + describe 'filter by last_activity_before' do + let(:params) { { last_activity_before: 60.minutes.ago } } + + before do + internal_project.update(last_activity_at: Time.now) + public_project.update(last_activity_at: 61.minutes.ago) + end + + it { is_expected.to match_array([public_project]) } + end + describe 'sorting' do let(:params) { { sort: 'name_asc' } } diff --git a/spec/frontend/helpers/user_mock_data_helper.js b/spec/frontend/helpers/user_mock_data_helper.js index 6999fa1f8a1..a6adc9dc3a0 100644 --- a/spec/frontend/helpers/user_mock_data_helper.js +++ b/spec/frontend/helpers/user_mock_data_helper.js @@ -1,14 +1,34 @@ +let id = 1; + +// Code taken from: https://gist.github.com/6174/6062387 +const getRandomString = () => + Math.random() + .toString(36) + .substring(2, 15) + + Math.random() + .toString(36) + .substring(2, 15); + +const getRandomUrl = () => `https://${getRandomString()}.com/${getRandomString()}`; + export default { createNumberRandomUsers(numberUsers) { const users = []; for (let i = 0; i < numberUsers; i += 1) { users.push({ - avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - id: i + 1, - name: `GitLab User ${i}`, - username: `gitlab${i}`, + avatar_url: getRandomUrl(), + id: id + 1, + name: getRandomString(), + username: getRandomString(), + user_path: getRandomUrl(), }); + + id += 1; } return users; }, + + createRandomUser() { + return this.createNumberRandomUsers(1)[0]; + }, }; diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js index 0cb182b2df4..3418680f8ea 100644 --- a/spec/frontend/sidebar/assignees_spec.js +++ b/spec/frontend/sidebar/assignees_spec.js @@ -101,14 +101,14 @@ describe('Assignee component', () => { const first = collapsedChildren.at(0); - expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar); + expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url); expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`); expect(trimText(first.find('.author').text())).toBe(users[0].name); const second = collapsedChildren.at(1); - expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar); + expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar_url); expect(second.find('.avatar').attributes('alt')).toBe(`${users[1].name}'s avatar`); expect(trimText(second.find('.author').text())).toBe(users[1].name); @@ -127,7 +127,7 @@ describe('Assignee component', () => { const first = collapsedChildren.at(0); - expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar); + expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url); expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`); expect(trimText(first.find('.author').text())).toBe(users[0].name); diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap index 4f1d46dffef..be75a5bfbdc 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap @@ -23,7 +23,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = id="visibility-level-setting" > <gl-form-radio-group-stub - checked="0" + checked="private" disabledfield="disabled" htmlfield="html" options="" @@ -33,7 +33,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = > <gl-form-radio-stub class="mb-3" - value="0" + value="private" > <div class="d-flex align-items-center" @@ -44,7 +44,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = /> <span - class="font-weight-bold ml-1" + class="font-weight-bold ml-1 js-visibility-option" > Private </span> @@ -52,7 +52,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = </gl-form-radio-stub> <gl-form-radio-stub class="mb-3" - value="1" + value="internal" > <div class="d-flex align-items-center" @@ -63,7 +63,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = /> <span - class="font-weight-bold ml-1" + class="font-weight-bold ml-1 js-visibility-option" > Internal </span> @@ -71,7 +71,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = </gl-form-radio-stub> <gl-form-radio-stub class="mb-3" - value="2" + value="public" > <div class="d-flex align-items-center" @@ -82,7 +82,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = /> <span - class="font-weight-bold ml-1" + class="font-weight-bold ml-1 js-visibility-option" > Public </span> diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js index 42b49c50c75..334fe7196a4 100644 --- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js @@ -2,18 +2,21 @@ import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue'; import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue'; import BlobContentEdit from '~/blob/components/blob_edit_content.vue'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; jest.mock('~/blob/utils', () => jest.fn()); describe('Snippet Blob Edit component', () => { let wrapper; - const content = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; + const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; const fileName = 'lorem.txt'; + const findHeader = () => wrapper.find(BlobHeaderEdit); + const findContent = () => wrapper.find(BlobContentEdit); function createComponent() { wrapper = shallowMount(SnippetBlobEdit, { propsData: { - content, + value, fileName, }, }); @@ -33,8 +36,20 @@ describe('Snippet Blob Edit component', () => { }); it('renders required components', () => { - expect(wrapper.contains(BlobHeaderEdit)).toBe(true); - expect(wrapper.contains(BlobContentEdit)).toBe(true); + expect(findHeader().exists()).toBe(true); + expect(findContent().exists()).toBe(true); + }); + }); + + describe('functionality', () => { + it('emits "name-change" event when the file name gets changed', () => { + expect(wrapper.emitted('name-change')).toBeUndefined(); + const newFilename = 'foo.bar'; + findHeader().vm.$emit('input', newFilename); + + return nextTick().then(() => { + expect(wrapper.emitted('name-change')[0]).toEqual([newFilename]); + }); }); }); }); diff --git a/spec/frontend/snippets/components/snippet_description_edit_spec.js b/spec/frontend/snippets/components/snippet_description_edit_spec.js index 167489dc004..c5e667747c6 100644 --- a/spec/frontend/snippets/components/snippet_description_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_description_edit_spec.js @@ -6,11 +6,12 @@ describe('Snippet Description Edit component', () => { const defaultDescription = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; const markdownPreviewPath = 'foo/'; const markdownDocsPath = 'help/'; + const findTextarea = () => wrapper.find('textarea'); - function createComponent(description = defaultDescription) { + function createComponent(value = defaultDescription) { wrapper = shallowMount(SnippetDescriptionEdit, { propsData: { - description, + value, markdownPreviewPath, markdownDocsPath, }, @@ -49,4 +50,14 @@ describe('Snippet Description Edit component', () => { expect(isHidden('.js-expanded')).toBe(true); }); }); + + describe('functionality', () => { + it('emits "input" event when description is changed', () => { + expect(wrapper.emitted('input')).toBeUndefined(); + const newDescription = 'dummy'; + findTextarea().setValue(newDescription); + + expect(wrapper.emitted('input')[0]).toEqual([newDescription]); + }); + }); }); diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js index 5104d742bb3..0bdef71bc08 100644 --- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js @@ -1,37 +1,42 @@ import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue'; -import { GlFormRadio } from '@gitlab/ui'; -import { SNIPPET_VISIBILITY } from '~/snippets/constants'; +import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui'; +import { + SNIPPET_VISIBILITY, + SNIPPET_VISIBILITY_PRIVATE, + SNIPPET_VISIBILITY_INTERNAL, + SNIPPET_VISIBILITY_PUBLIC, +} from '~/snippets/constants'; import { mount, shallowMount } from '@vue/test-utils'; describe('Snippet Visibility Edit component', () => { let wrapper; - let radios; const defaultHelpLink = '/foo/bar'; - const defaultVisibilityLevel = '0'; + const defaultVisibilityLevel = 'private'; - function findElements(sel) { - return wrapper.findAll(sel); - } - - function createComponent( - { - helpLink = defaultHelpLink, - isProjectSnippet = false, - visibilityLevel = defaultVisibilityLevel, - } = {}, - deep = false, - ) { + function createComponent(propsData = {}, deep = false) { const method = deep ? mount : shallowMount; wrapper = method.call(this, SnippetVisibilityEdit, { propsData: { - helpLink, - isProjectSnippet, - visibilityLevel, + helpLink: defaultHelpLink, + isProjectSnippet: false, + value: defaultVisibilityLevel, + ...propsData, }, }); - radios = findElements(GlFormRadio); } + const findLabel = () => wrapper.find('label'); + const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio); + const findRadiosData = () => + findRadios().wrappers.map(x => { + return { + value: x.find('input').attributes('value'), + icon: x.find(GlIcon).props('name'), + description: x.find('.help-text').text(), + text: x.find('.js-visibility-option').text(), + }; + }); + afterEach(() => { wrapper.destroy(); }); @@ -42,53 +47,66 @@ describe('Snippet Visibility Edit component', () => { expect(wrapper.element).toMatchSnapshot(); }); - it.each` - label | value - ${SNIPPET_VISIBILITY.private.label} | ${`0`} - ${SNIPPET_VISIBILITY.internal.label} | ${`1`} - ${SNIPPET_VISIBILITY.public.label} | ${`2`} - `('should render correct $label label', ({ label, value }) => { - createComponent(); - const radio = radios.at(parseInt(value, 10)); + it('renders visibility options', () => { + createComponent({}, true); - expect(radio.attributes('value')).toBe(value); - expect(radio.text()).toContain(label); + expect(findRadiosData()).toEqual([ + { + value: SNIPPET_VISIBILITY_PRIVATE, + icon: SNIPPET_VISIBILITY.private.icon, + text: SNIPPET_VISIBILITY.private.label, + description: SNIPPET_VISIBILITY.private.description, + }, + { + value: SNIPPET_VISIBILITY_INTERNAL, + icon: SNIPPET_VISIBILITY.internal.icon, + text: SNIPPET_VISIBILITY.internal.label, + description: SNIPPET_VISIBILITY.internal.description, + }, + { + value: SNIPPET_VISIBILITY_PUBLIC, + icon: SNIPPET_VISIBILITY.public.icon, + text: SNIPPET_VISIBILITY.public.label, + description: SNIPPET_VISIBILITY.public.description, + }, + ]); }); - describe('rendered help-text', () => { - it.each` - description | value | label - ${SNIPPET_VISIBILITY.private.description} | ${`0`} | ${SNIPPET_VISIBILITY.private.label} - ${SNIPPET_VISIBILITY.internal.description} | ${`1`} | ${SNIPPET_VISIBILITY.internal.label} - ${SNIPPET_VISIBILITY.public.description} | ${`2`} | ${SNIPPET_VISIBILITY.public.label} - `('should render correct $label description', ({ description, value }) => { - createComponent({}, true); - - const help = findElements('.help-text').at(parseInt(value, 10)); + it('when project snippet, renders special private description', () => { + createComponent({ isProjectSnippet: true }, true); - expect(help.text()).toBe(description); + expect(findRadiosData()[0]).toEqual({ + value: SNIPPET_VISIBILITY_PRIVATE, + icon: SNIPPET_VISIBILITY.private.icon, + text: SNIPPET_VISIBILITY.private.label, + description: SNIPPET_VISIBILITY.private.description_project, }); + }); + + it('renders label help link', () => { + createComponent(); - it('renders correct Private description for a project snippet', () => { - createComponent({ isProjectSnippet: true }, true); + expect( + findLabel() + .find(GlLink) + .attributes('href'), + ).toBe(defaultHelpLink); + }); - const helpText = findElements('.help-text') - .at(0) - .text(); + it('when helpLink is not defined, does not render label help link', () => { + createComponent({ helpLink: null }); - expect(helpText).not.toContain(SNIPPET_VISIBILITY.private.description); - expect(helpText).toBe(SNIPPET_VISIBILITY.private.description_project); - }); + expect(findLabel().contains(GlLink)).toBe(false); }); }); describe('functionality', () => { it('pre-selects correct option in the list', () => { - const pos = 1; + const value = SNIPPET_VISIBILITY_INTERNAL; + + createComponent({ value }); - createComponent({ visibilityLevel: `${pos}` }, true); - const radio = radios.at(pos); - expect(radio.find('input[type="radio"]').element.checked).toBe(true); + expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value); }); }); }); diff --git a/spec/lib/gitlab/jira_import/labels_importer_spec.rb b/spec/lib/gitlab/jira_import/labels_importer_spec.rb new file mode 100644 index 00000000000..eaa13d9ed32 --- /dev/null +++ b/spec/lib/gitlab/jira_import/labels_importer_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::JiraImport::LabelsImporter do + let(:user) { create(:user) } + let(:jira_import_data) do + data = JiraImportData.new + data << JiraImportData::JiraProjectDetails.new('XX', Time.now.strftime('%Y-%m-%d %H:%M:%S'), { user_id: user.id, name: user.name }) + data + end + let(:project) { create(:project, import_data: jira_import_data) } + let!(:jira_service) { create(:jira_service, project: project) } + + subject { described_class.new(project).execute } + + before do + stub_feature_flags(jira_issue_import: true) + end + + describe '#execute', :clean_gitlab_redis_cache do + context 'when label creation failes' do + before do + allow_next_instance_of(Labels::CreateService) do |instance| + allow(instance).to receive(:execute).and_return(nil) + end + end + + it 'raises error' do + expect { subject }.to raise_error(Projects::ImportService::Error, 'Failed to create import label for jira import.') + end + end + + context 'when label is created successfully' do + it 'creates import label' do + expect { subject }.to change { Label.count }.by(1) + end + + it 'caches import label' do + expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.import_label_cache_key(project.id))).to be nil + + subject + + expect(Gitlab::JiraImport.get_import_label_id(project.id).to_i).to be > 0 + end + end + end +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index c148f5e63a5..eca69d755cc 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -147,6 +147,8 @@ describe Gitlab::UsageData, :aggregate_failures do subject { described_class.components_usage_data } it 'gathers components usage data' do + expect(Gitlab::UsageData).to receive(:app_server_type).and_return('server_type') + expect(subject[:app_server][:type]).to eq('server_type') expect(subject[:gitlab_pages][:enabled]).to eq(Gitlab.config.pages.enabled) expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION) expect(subject[:git][:version]).to eq(Gitlab::Git.version) @@ -159,6 +161,28 @@ describe Gitlab::UsageData, :aggregate_failures do end end + describe '#app_server_type' do + subject { described_class.app_server_type } + + it 'successfully identifies runtime and returns the identifier' do + expect(Gitlab::Runtime).to receive(:identify).and_return(:runtime_identifier) + + is_expected.to eq('runtime_identifier') + end + + context 'when runtime is not identified' do + let(:exception) { Gitlab::Runtime::IdentificationError.new('exception message from runtime identify') } + + it 'logs the exception and returns unknown app server type' do + expect(Gitlab::Runtime).to receive(:identify).and_raise(exception) + + expect(Gitlab::AppLogger).to receive(:error).with(exception.message) + expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception) + expect(subject).to eq('unknown_app_server_type') + end + end + end + describe '#cycle_analytics_usage_data' do subject { described_class.cycle_analytics_usage_data } diff --git a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb index c09492efcae..b6db803ddf5 100644 --- a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb +++ b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb @@ -32,12 +32,28 @@ describe Gitlab::JiraImport::ImportIssueWorker do end context 'when record is successfully inserted' do - before do - subject.perform(project.id, 123, issue_attrs, 'some-key') + let(:label) { create(:label, project: project) } + + context 'when import label does not exist' do + it 'does not record import failure' do + subject.perform(project.id, 123, issue_attrs, 'some-key') + + expect(label.issues.count).to eq(0) + expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.failed_issues_counter_cache_key(project.id)).to_i).to eq(0) + end end - it 'does not record import failure' do - expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.failed_issues_counter_cache_key(project.id)).to_i).to eq(0) + context 'when import label exists' do + before do + Gitlab::JiraImport.cache_import_label_id(project.id, label.id) + end + + it 'does not record import failure' do + subject.perform(project.id, 123, issue_attrs, 'some-key') + + expect(label.issues.count).to eq(1) + expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.failed_issues_counter_cache_key(project.id)).to_i).to eq(0) + end end end end diff --git a/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb index 2b156e0f489..a3e38cba115 100644 --- a/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb +++ b/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe Gitlab::JiraImport::Stage::ImportLabelsWorker do + let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } describe 'modules' do @@ -30,9 +31,24 @@ describe Gitlab::JiraImport::Stage::ImportLabelsWorker do end context 'when import started' do + let(:jira_import_data) do + data = JiraImportData.new + data << JiraImportData::JiraProjectDetails.new('XX', Time.now.strftime('%Y-%m-%d %H:%M:%S'), { user_id: user.id, name: user.name }) + data + end + let(:project) { create(:project, import_data: jira_import_data) } + let!(:jira_service) { create(:jira_service, project: project) } let!(:import_state) { create(:import_state, status: :started, project: project) } it_behaves_like 'advance to next stage', :issues + + it 'executes labels importer' do + expect_next_instance_of(Gitlab::JiraImport::LabelsImporter) do |instance| + expect(instance).to receive(:execute).and_return(Gitlab::JobWaiter.new) + end + + described_class.new.perform(project.id) + end end end end |