summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-01 09:07:45 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-01 09:07:45 +0000
commitb11f7057d067885619ee3e513751f180b2e8ad85 (patch)
treedfb3077ea8716ed217f5ce4324be4e25a450c599
parente50050a8756a20b6aa118edbad3369674e4c63ba (diff)
downloadgitlab-ce-b11f7057d067885619ee3e513751f180b2e8ad85.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml1
-rw-r--r--.rubocop.yml1
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue1
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue21
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_edit.vue22
-rw-r--r--app/assets/javascripts/snippets/components/snippet_visibility_edit.vue60
-rw-r--r--app/assets/javascripts/snippets/constants.js9
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss4
-rw-r--r--app/finders/projects_finder.rb20
-rw-r--r--app/models/label.rb5
-rw-r--r--app/workers/gitlab/jira_import/import_issue_worker.rb32
-rw-r--r--app/workers/gitlab/jira_import/stage/import_labels_worker.rb6
-rw-r--r--changelogs/unreleased/208502-add-app-server-type-to-usage-ping.yml5
-rw-r--r--changelogs/unreleased/feat-api-project-last-activity.yml5
-rw-r--r--changelogs/unreleased/pedroms-fix-mr-threads-icon-button-color.yml5
-rw-r--r--doc/api/projects.md42
-rw-r--r--doc/ci/git_submodules.md2
-rw-r--r--doc/ci/junit_test_reports.md2
-rw-r--r--doc/ci/multi_project_pipelines.md2
-rw-r--r--doc/development/documentation/styleguide.md6
-rw-r--r--doc/integration/elasticsearch.md16
-rw-r--r--doc/topics/autodevops/index.md4
-rw-r--r--doc/user/discussions/index.md4
-rw-r--r--doc/user/project/canary_deployments.md2
-rw-r--r--doc/user/project/integrations/prometheus.md2
-rw-r--r--lib/api/helpers.rb16
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/gitlab/jira_import.rb35
-rw-r--r--lib/gitlab/jira_import/labels_importer.rb42
-rw-r--r--lib/gitlab/usage_data.rb11
-rw-r--r--locale/gitlab.pot21
-rw-r--r--spec/finders/projects_finder_spec.rb22
-rw-r--r--spec/frontend/helpers/user_mock_data_helper.js28
-rw-r--r--spec/frontend/sidebar/assignees_spec.js6
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap14
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js23
-rw-r--r--spec/frontend/snippets/components/snippet_description_edit_spec.js15
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js124
-rw-r--r--spec/lib/gitlab/jira_import/labels_importer_spec.rb48
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb24
-rw-r--r--spec/workers/gitlab/jira_import/import_issue_worker_spec.rb24
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb16
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