Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue4
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue29
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue6
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue2
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue16
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss63
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/graphql/types/snippets/blob_type.rb4
-rw-r--r--app/models/repository.rb42
-rw-r--r--changelogs/unreleased/26113-webide-upload-encoding.yml5
-rw-r--r--changelogs/unreleased/fj-add-mode-field-to-snippet-blob-type.yml5
-rw-r--r--changelogs/unreleased/refactoring-entities-file-10.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json14
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/development/api_styleguide.md2
-rw-r--r--lib/api/entities.rb121
-rw-r--r--lib/api/entities/external_issue.rb10
-rw-r--r--lib/api/entities/issuable_time_stats.rb26
-rw-r--r--lib/api/entities/issue.rb48
-rw-r--r--lib/api/entities/issue_basic.rb43
-rw-r--r--lib/api/entities/milestone.rb19
-rw-r--r--lib/gitlab/experimentation.rb6
-rw-r--r--lib/gitlab/repository_cache.rb4
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap4
-rw-r--r--spec/frontend/ide/mock_data.js6
-rw-r--r--spec/graphql/types/snippets/blob_type_spec.rb3
-rw-r--r--spec/javascripts/ide/components/new_dropdown/upload_spec.js41
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js28
-rw-r--r--spec/lib/gitlab/repository_cache_spec.rb12
-rw-r--r--spec/models/repository_spec.rb97
-rw-r--r--spec/requests/api/branches_spec.rb2
36 files changed, 454 insertions, 234 deletions
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
index 7280fba9e7a..9c0c97bc5ae 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/description.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -26,7 +26,7 @@ export default {
<ci-icon :status="job.status" :borderless="true" :size="24" class="d-flex" />
<span class="prepend-left-8">
{{ job.name }}
- <a :href="job.path" target="_blank" class="ide-external-link">
+ <a :href="job.path" target="_blank" class="ide-external-link position-relative">
{{ jobId }} <icon :size="12" name="external-link" />
</a>
</span>
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 52ca61c06b0..ba8407382f4 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -71,7 +71,7 @@ export default {
v-tooltip="showTooltip"
:title="showTooltip ? stage.name : null"
data-container="body"
- class="prepend-left-8 ide-stage-title"
+ class="prepend-left-8 text-truncate"
>
{{ stage.name }}
</strong>
@@ -80,7 +80,7 @@ export default {
</div>
<icon :name="collapseIcon" class="ide-stage-collapse-icon" />
</div>
- <div v-show="!stage.isCollapsed" ref="jobList" class="card-body">
+ <div v-show="!stage.isCollapsed" ref="jobList" class="card-body p-0">
<gl-loading-icon v-if="showLoadingIcon" />
<template v-else>
<item v-for="job in stage.jobs" :key="job.id" :job="job" @clickViewLog="clickViewLog" />
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index b2fa020fb00..bcaaa8e09c2 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -51,7 +51,7 @@ export default {
</script>
<template>
- <div class="ide-new-btn">
+ <div class="ide-new-btn d-none">
<div
:class="{
show: isOpen,
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index e52613086a4..0efb0012246 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -43,21 +43,28 @@ export default {
},
createFile(target, file) {
const { name } = file;
- let { result } = target;
- const encodedContent = result.split('base64,')[1];
+ const encodedContent = target.result.split('base64,')[1];
const rawContent = encodedContent ? atob(encodedContent) : '';
const isText = this.isText(rawContent, file.type);
- result = isText ? rawContent : encodedContent;
+ const emitCreateEvent = content =>
+ this.$emit('create', {
+ name: `${this.path ? `${this.path}/` : ''}${name}`,
+ type: 'blob',
+ content,
+ base64: !isText,
+ binary: !isText,
+ rawPath: !isText ? target.result : '',
+ });
- this.$emit('create', {
- name: `${this.path ? `${this.path}/` : ''}${name}`,
- type: 'blob',
- content: result,
- base64: !isText,
- binary: !isText,
- rawPath: !isText ? target.result : '',
- });
+ if (isText) {
+ const reader = new FileReader();
+
+ reader.addEventListener('load', e => emitCreateEvent(e.target.result), { once: true });
+ reader.readAsText(file);
+ } else {
+ emitCreateEvent(encodedContent);
+ }
},
readFile(file) {
const reader = new FileReader();
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 5ae73b2fc9c..b61d0a47795 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -62,7 +62,11 @@ export default {
<ci-icon :status="latestPipeline.details.status" :size="24" />
<span class="prepend-left-8">
<strong> {{ __('Pipeline') }} </strong>
- <a :href="latestPipeline.path" target="_blank" class="ide-external-link">
+ <a
+ :href="latestPipeline.path"
+ target="_blank"
+ class="ide-external-link position-relative"
+ >
#{{ latestPipeline.id }} <icon :size="12" name="external-link" />
</a>
</span>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 7e2ab96d1de..c8c3036812e 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -274,7 +274,7 @@ export default {
<template>
<div id="ide" class="blob-viewer-container blob-editor-container">
<div class="ide-mode-tabs clearfix">
- <ul v-if="!shouldHideEditor && isEditModeActive" class="nav-links float-left">
+ <ul v-if="!shouldHideEditor && isEditModeActive" class="nav-links float-left border-bottom-0">
<li :class="editTabCSS">
<a
href="javascript:void(0);"
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
index 0eba9c39817..7576b2477d1 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
@@ -14,9 +14,10 @@ export default {
iid: mergeRequest.iid,
title: mergeRequest.title,
projectId: mergeRequest.project_id,
- projectPathWithNamespace: mergeRequest.web_url
- .replace(`${gon.gitlab_url}/`, '')
- .replace(`/merge_requests/${mergeRequest.iid}`, ''),
+ projectPathWithNamespace: mergeRequest.references.full.replace(
+ mergeRequest.references.short,
+ '',
+ ),
}));
},
[types.RESET_MERGE_REQUESTS](state) {
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index fe1a2a092ad..e80cb06edfb 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -13,6 +13,11 @@ export default {
type: String,
required: true,
},
+ filePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
fileSize: {
type: Number,
required: false,
@@ -24,7 +29,8 @@ export default {
return numberToHumanSize(this.fileSize);
},
fileName() {
- return this.path.split('/').pop();
+ // path could be a base64 uri too, so check if filePath was passed additionally
+ return (this.filePath || this.path).split('/').pop();
},
},
};
@@ -39,7 +45,13 @@ export default {
({{ fileSizeReadable }})
</template>
</p>
- <gl-link :href="path" class="btn btn-default" rel="nofollow" download target="_blank">
+ <gl-link
+ :href="path"
+ class="btn btn-default"
+ rel="nofollow"
+ :download="fileName"
+ target="_blank"
+ >
<icon :size="16" name="download" class="float-left append-right-8" />
{{ __('Download') }}
</gl-link>
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 420271c9a1e..5eaff7702f6 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -25,10 +25,6 @@ $ide-commit-header-height: 48px;
@include str-truncated(250px);
}
-.editable-mode {
- display: inline-block;
-}
-
.ide-view {
position: relative;
margin-top: 0;
@@ -332,23 +328,6 @@ $ide-commit-header-height: 48px;
padding: $gl-padding;
max-width: 100%;
max-height: 100%;
-
- img {
- max-width: 90%;
- }
-
- .isZoomable {
- cursor: pointer;
- cursor: zoom-in;
-
- &.isZoomed {
- cursor: pointer;
- cursor: zoom-out;
- max-width: none;
- max-height: none;
- margin-right: $gl-padding;
- }
- }
}
.file-info {
@@ -361,13 +340,9 @@ $ide-commit-header-height: 48px;
.ide-mode-tabs {
border-bottom: 1px solid $white-dark;
- .nav-links {
- border-bottom: 0;
-
- li a {
- padding: $gl-padding-8 $gl-padding;
- line-height: $gl-btn-line-height;
- }
+ li a {
+ padding: $gl-padding-8 $gl-padding;
+ line-height: $gl-btn-line-height;
}
}
@@ -564,12 +539,6 @@ $ide-commit-header-height: 48px;
background: $gray-100;
outline: 0;
-
- .multi-file-discard-btn {
- > .btn {
- display: flex;
- }
- }
}
&:active {
@@ -596,18 +565,6 @@ $ide-commit-header-height: 48px;
}
}
-.multi-file-discard-btn {
- > .btn {
- display: none;
- width: $ide-commit-row-height;
- height: $ide-commit-row-height;
- }
-
- svg {
- top: 0;
- }
-}
-
.multi-file-commit-form {
position: relative;
background-color: $white-light;
@@ -1060,8 +1017,6 @@ $ide-commit-header-height: 48px;
}
.ide-external-link {
- position: relative;
-
svg {
display: none;
position: absolute;
@@ -1164,22 +1119,12 @@ $ide-commit-header-height: 48px;
align-items: center;
}
}
-
- .card-body {
- padding: 0;
- }
}
.ide-stage-collapse-icon {
margin: auto 0 auto auto;
}
-.ide-stage-title {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
.ide-job-header {
min-height: 60px;
}
@@ -1279,8 +1224,6 @@ $ide-commit-header-height: 48px;
}
.ide-new-btn {
- display: none;
-
.btn {
padding: 2px 5px;
}
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index e21930c9cfe..8c0188e1783 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -21,6 +21,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action only: [:show] do
push_frontend_feature_flag(:diffs_batch_load, @project)
push_frontend_feature_flag(:single_mr_diff_view, @project)
+ push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
end
before_action do
diff --git a/app/graphql/types/snippets/blob_type.rb b/app/graphql/types/snippets/blob_type.rb
index f398fe9c121..cacb2177192 100644
--- a/app/graphql/types/snippets/blob_type.rb
+++ b/app/graphql/types/snippets/blob_type.rb
@@ -40,6 +40,10 @@ module Types
field :rich_viewer, type: Types::Snippets::BlobViewerType,
description: 'Blob content rich viewer',
null: true
+
+ field :mode, type: GraphQL::STRING_TYPE,
+ description: 'Blob mode',
+ null: true
end
# rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e7ad38864c8..c6573e0bad2 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -41,8 +41,8 @@ class Repository
CACHED_METHODS = %i(size commit_count rendered_readme readme_path contribution_guide
changelog license_blob license_key gitignore
gitlab_ci_yml branch_names tag_names branch_count
- tag_count avatar exists? root_ref has_visible_content?
- issue_template_names merge_request_template_names
+ tag_count avatar exists? root_ref merged_branch_names
+ has_visible_content? issue_template_names merge_request_template_names
metrics_dashboard_paths xcode_project?).freeze
# Methods that use cache_method but only memoize the value
@@ -65,6 +65,8 @@ class Repository
xcode_config: :xcode_project?
}.freeze
+ MERGED_BRANCH_NAMES_CACHE_DURATION = 10.minutes
+
def initialize(full_path, project, disk_path: nil, repo_type: Gitlab::GlRepository::PROJECT)
@full_path = full_path
@disk_path = disk_path || full_path
@@ -296,7 +298,7 @@ class Repository
end
def expire_branches_cache
- expire_method_caches(%i(branch_names branch_count has_visible_content?))
+ expire_method_caches(%i(branch_names merged_branch_names branch_count has_visible_content?))
@local_branches = nil
@branch_exists_memo = nil
end
@@ -916,7 +918,39 @@ class Repository
@root_ref_sha ||= commit(root_ref).sha
end
- delegate :merged_branch_names, to: :raw_repository
+ def merged_branch_names(branch_names = [])
+ # Currently we should skip caching if requesting all branch names
+ # This is only used in a few places, notably app/services/branches/delete_merged_service.rb,
+ # and it could potentially result in a very large cache/performance issues with the current
+ # implementation.
+ skip_cache = branch_names.empty? || Feature.disabled?(:merged_branch_names_redis_caching)
+ return raw_repository.merged_branch_names(branch_names) if skip_cache
+
+ cached_branch_names = cache.read(:merged_branch_names)
+ merged_branch_names_hash = cached_branch_names || {}
+ missing_branch_names = branch_names.select { |bn| !merged_branch_names_hash.key?(bn) }
+
+ # Track some metrics here whilst feature flag is enabled
+ if cached_branch_names.present?
+ counter = Gitlab::Metrics.counter(
+ :gitlab_repository_merged_branch_names_cache_hit,
+ "Count of cache hits for Repository#merged_branch_names"
+ )
+ counter.increment(full_hit: missing_branch_names.empty?)
+ end
+
+ if missing_branch_names.any?
+ merged = raw_repository.merged_branch_names(missing_branch_names)
+
+ missing_branch_names.each do |bn|
+ merged_branch_names_hash[bn] = merged.include?(bn)
+ end
+
+ cache.write(:merged_branch_names, merged_branch_names_hash, expires_in: MERGED_BRANCH_NAMES_CACHE_DURATION)
+ end
+
+ Set.new(merged_branch_names_hash.select { |_, v| v }.keys)
+ end
def merge_base(*commits_or_ids)
commit_ids = commits_or_ids.map do |commit_or_id|
diff --git a/changelogs/unreleased/26113-webide-upload-encoding.yml b/changelogs/unreleased/26113-webide-upload-encoding.yml
new file mode 100644
index 00000000000..f42973f36de
--- /dev/null
+++ b/changelogs/unreleased/26113-webide-upload-encoding.yml
@@ -0,0 +1,5 @@
+---
+title: Fix some of the file encoding issues when uploading in the Web IDE
+merge_request: 23761
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-add-mode-field-to-snippet-blob-type.yml b/changelogs/unreleased/fj-add-mode-field-to-snippet-blob-type.yml
new file mode 100644
index 00000000000..03ed04d9057
--- /dev/null
+++ b/changelogs/unreleased/fj-add-mode-field-to-snippet-blob-type.yml
@@ -0,0 +1,5 @@
+---
+title: Add mode field to snippet blob in GraphQL
+merge_request: 24157
+author:
+type: changed
diff --git a/changelogs/unreleased/refactoring-entities-file-10.yml b/changelogs/unreleased/refactoring-entities-file-10.yml
new file mode 100644
index 00000000000..bc78c6d4fa6
--- /dev/null
+++ b/changelogs/unreleased/refactoring-entities-file-10.yml
@@ -0,0 +1,5 @@
+---
+title: Separate issue entities into own class files
+merge_request: 24226
+author: Rajendra Kadam
+type: added
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index eb728233379..2c63ecfe08e 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -6642,6 +6642,11 @@ type SnippetBlob {
highlightedData: String
"""
+ Blob mode
+ """
+ mode: String
+
+ """
Blob name
"""
name: String
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index d346c8a886f..a35bf8caccf 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -7172,6 +7172,20 @@
"deprecationReason": null
},
{
+ "name": "mode",
+ "description": "Blob mode",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "name",
"description": "Blob name",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 7360ce0978a..a0d1787e816 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1063,6 +1063,7 @@ Represents the snippet blob
| --- | ---- | ---------- |
| `binary` | Boolean! | Shows whether the blob is binary |
| `highlightedData` | String | Blob highlighted data |
+| `mode` | String | Blob mode |
| `name` | String | Blob name |
| `path` | String | Blob path |
| `rawPath` | String! | Blob raw content endpoint path |
diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md
index d5fc24c1ddb..2510358b4d5 100644
--- a/doc/development/api_styleguide.md
+++ b/doc/development/api_styleguide.md
@@ -19,7 +19,7 @@ for a good example):
- `desc` for the method summary. You should pass it a block for additional
details such as:
- - The GitLab version when the endpoint was added
+ - The GitLab version when the endpoint was added. If it is behind a feature flag, mention that instead: _This feature is gated by the :feature\_flag\_symbol feature flag._
- If the endpoint is deprecated, and if so, when will it be removed
- `params` for the method params. This acts as description,
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index bd7b2fd9433..2da9a042978 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -128,127 +128,6 @@ module API
end
end
- class Milestone < Grape::Entity
- expose :id, :iid
- expose :project_id, if: -> (entity, options) { entity&.project_id }
- expose :group_id, if: -> (entity, options) { entity&.group_id }
- expose :title, :description
- expose :state, :created_at, :updated_at
- expose :due_date
- expose :start_date
-
- expose :web_url do |milestone, _options|
- Gitlab::UrlBuilder.build(milestone)
- end
- end
-
- class IssueBasic < IssuableEntity
- expose :closed_at
- expose :closed_by, using: Entities::UserBasic
-
- expose :labels do |issue, options|
- if options[:with_labels_details]
- ::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title))
- else
- issue.labels.map(&:title).sort
- end
- end
-
- expose :milestone, using: Entities::Milestone
- expose :assignees, :author, using: Entities::UserBasic
-
- expose :assignee, using: ::API::Entities::UserBasic do |issue|
- issue.assignees.first
- end
-
- expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) }
- expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count, options[:current_user]) }
- expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) }
- expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) }
- expose :due_date
- expose :confidential
- expose :discussion_locked
-
- expose :web_url do |issue|
- Gitlab::UrlBuilder.build(issue)
- end
-
- expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |issue|
- issue
- end
-
- expose :task_completion_status
- end
-
- class Issue < IssueBasic
- include ::API::Helpers::RelatedResourcesHelpers
-
- expose(:has_tasks) do |issue, _|
- !issue.task_list_items.empty?
- end
-
- expose :task_status, if: -> (issue, _) do
- !issue.task_list_items.empty?
- end
-
- expose :_links do
- expose :self do |issue|
- expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid))
- end
-
- expose :notes do |issue|
- expose_url(api_v4_projects_issues_notes_path(id: issue.project_id, noteable_id: issue.iid))
- end
-
- expose :award_emoji do |issue|
- expose_url(api_v4_projects_issues_award_emoji_path(id: issue.project_id, issue_iid: issue.iid))
- end
-
- expose :project do |issue|
- expose_url(api_v4_projects_path(id: issue.project_id))
- end
- end
-
- expose :references, with: IssuableReferences do |issue|
- issue
- end
-
- # Calculating the value of subscribed field triggers Markdown
- # processing. We can't do that for multiple issues / merge
- # requests in a single API request.
- expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |issue, options|
- issue.subscribed?(options[:current_user], options[:project] || issue.project)
- end
-
- expose :moved_to_id
- end
-
- class IssuableTimeStats < Grape::Entity
- format_with(:time_tracking_formatter) do |time_spent|
- Gitlab::TimeTrackingFormatter.output(time_spent)
- end
-
- expose :time_estimate
- expose :total_time_spent
- expose :human_time_estimate
-
- with_options(format_with: :time_tracking_formatter) do
- expose :total_time_spent, as: :human_total_time_spent
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def total_time_spent
- # Avoids an N+1 query since timelogs are preloaded
- object.timelogs.map(&:time_spent).sum
- end
- # rubocop: enable CodeReuse/ActiveRecord
- end
-
- class ExternalIssue < Grape::Entity
- expose :title
- expose :id
- end
-
class PipelineBasic < Grape::Entity
expose :id, :sha, :ref, :status
expose :created_at, :updated_at
diff --git a/lib/api/entities/external_issue.rb b/lib/api/entities/external_issue.rb
new file mode 100644
index 00000000000..8a201f70099
--- /dev/null
+++ b/lib/api/entities/external_issue.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ExternalIssue < Grape::Entity
+ expose :title
+ expose :id
+ end
+ end
+end
diff --git a/lib/api/entities/issuable_time_stats.rb b/lib/api/entities/issuable_time_stats.rb
new file mode 100644
index 00000000000..7c3452a10a1
--- /dev/null
+++ b/lib/api/entities/issuable_time_stats.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class IssuableTimeStats < Grape::Entity
+ format_with(:time_tracking_formatter) do |time_spent|
+ Gitlab::TimeTrackingFormatter.output(time_spent)
+ end
+
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+
+ with_options(format_with: :time_tracking_formatter) do
+ expose :total_time_spent, as: :human_total_time_spent
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def total_time_spent
+ # Avoids an N+1 query since timelogs are preloaded
+ object.timelogs.map(&:time_spent).sum
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/lib/api/entities/issue.rb b/lib/api/entities/issue.rb
new file mode 100644
index 00000000000..b7eb22b2aba
--- /dev/null
+++ b/lib/api/entities/issue.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Issue < IssueBasic
+ include ::API::Helpers::RelatedResourcesHelpers
+
+ expose(:has_tasks) do |issue, _|
+ !issue.task_list_items.empty?
+ end
+
+ expose :task_status, if: -> (issue, _) do
+ !issue.task_list_items.empty?
+ end
+
+ expose :_links do
+ expose :self do |issue|
+ expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid))
+ end
+
+ expose :notes do |issue|
+ expose_url(api_v4_projects_issues_notes_path(id: issue.project_id, noteable_id: issue.iid))
+ end
+
+ expose :award_emoji do |issue|
+ expose_url(api_v4_projects_issues_award_emoji_path(id: issue.project_id, issue_iid: issue.iid))
+ end
+
+ expose :project do |issue|
+ expose_url(api_v4_projects_path(id: issue.project_id))
+ end
+ end
+
+ expose :references, with: IssuableReferences do |issue|
+ issue
+ end
+
+ # Calculating the value of subscribed field triggers Markdown
+ # processing. We can't do that for multiple issues / merge
+ # requests in a single API request.
+ expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |issue, options|
+ issue.subscribed?(options[:current_user], options[:project] || issue.project)
+ end
+
+ expose :moved_to_id
+ end
+ end
+end
diff --git a/lib/api/entities/issue_basic.rb b/lib/api/entities/issue_basic.rb
new file mode 100644
index 00000000000..7e4be35d20b
--- /dev/null
+++ b/lib/api/entities/issue_basic.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class IssueBasic < IssuableEntity
+ expose :closed_at
+ expose :closed_by, using: Entities::UserBasic
+
+ expose :labels do |issue, options|
+ if options[:with_labels_details]
+ ::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title))
+ else
+ issue.labels.map(&:title).sort
+ end
+ end
+
+ expose :milestone, using: Entities::Milestone
+ expose :assignees, :author, using: Entities::UserBasic
+
+ expose :assignee, using: ::API::Entities::UserBasic do |issue|
+ issue.assignees.first
+ end
+
+ expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) }
+ expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count, options[:current_user]) }
+ expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) }
+ expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) }
+ expose :due_date
+ expose :confidential
+ expose :discussion_locked
+
+ expose :web_url do |issue|
+ Gitlab::UrlBuilder.build(issue)
+ end
+
+ expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |issue|
+ issue
+ end
+
+ expose :task_completion_status
+ end
+ end
+end
diff --git a/lib/api/entities/milestone.rb b/lib/api/entities/milestone.rb
new file mode 100644
index 00000000000..5a0c222d691
--- /dev/null
+++ b/lib/api/entities/milestone.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Milestone < Grape::Entity
+ expose :id, :iid
+ expose :project_id, if: -> (entity, options) { entity&.project_id }
+ expose :group_id, if: -> (entity, options) { entity&.group_id }
+ expose :title, :description
+ expose :state, :created_at, :updated_at
+ expose :due_date
+ expose :start_date
+
+ expose :web_url do |milestone, _options|
+ Gitlab::UrlBuilder.build(milestone)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 2066b58dff5..9ceaa742f51 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -22,6 +22,12 @@ module Gitlab
environment: ::Gitlab.dev_env_or_com?,
enabled_ratio: 0.25,
tracking_category: 'Growth::Acquisition::Experiment::PaidSignUpFlow'
+ },
+ suggest_pipeline: {
+ feature_toggle: :suggest_pipeline,
+ environment: ::Gitlab.dev_env_or_com?,
+ enabled_ratio: 0.1,
+ tracking_category: 'Growth::Expansion::Experiment::SuggestPipeline'
}
}.freeze
diff --git a/lib/gitlab/repository_cache.rb b/lib/gitlab/repository_cache.rb
index fca8c43da2e..dc8b2467f72 100644
--- a/lib/gitlab/repository_cache.rb
+++ b/lib/gitlab/repository_cache.rb
@@ -33,8 +33,8 @@ module Gitlab
backend.read(cache_key(key))
end
- def write(key, value)
- backend.write(cache_key(key), value)
+ def write(key, value, *args)
+ backend.write(cache_key(key), value, *args)
end
def fetch_without_caching_false(key, &block)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 260c3d2625b..c91bae519e5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2104,6 +2104,9 @@ msgstr ""
msgid "Approver"
msgstr ""
+msgid "Approvers"
+msgstr ""
+
msgid "Apr"
msgstr ""
diff --git a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
index 43e606eac6e..db5175c3f7b 100644
--- a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
+++ b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
@@ -14,7 +14,7 @@ exports[`IDE pipeline stage renders stage details & icon 1`] = `
/>
<strong
- class="prepend-left-8 ide-stage-title"
+ class="prepend-left-8 text-truncate"
data-container="body"
data-original-title=""
title=""
@@ -42,7 +42,7 @@ exports[`IDE pipeline stage renders stage details & icon 1`] = `
</div>
<div
- class="card-body"
+ class="card-body p-0"
>
<item-stub
job="[object Object]"
diff --git a/spec/frontend/ide/mock_data.js b/spec/frontend/ide/mock_data.js
index 80eb15fe5a6..a1b57dca6bc 100644
--- a/spec/frontend/ide/mock_data.js
+++ b/spec/frontend/ide/mock_data.js
@@ -165,7 +165,11 @@ export const mergeRequests = [
iid: 1,
title: 'Test merge request',
project_id: 1,
- web_url: `${TEST_HOST}/namespace/project-path/merge_requests/1`,
+ web_url: `${TEST_HOST}/namespace/project-path/-/merge_requests/1`,
+ references: {
+ short: '!1',
+ full: 'namespace/project-path!1',
+ },
},
];
diff --git a/spec/graphql/types/snippets/blob_type_spec.rb b/spec/graphql/types/snippets/blob_type_spec.rb
index f1837538b53..e7d4e5dfa2d 100644
--- a/spec/graphql/types/snippets/blob_type_spec.rb
+++ b/spec/graphql/types/snippets/blob_type_spec.rb
@@ -6,7 +6,8 @@ describe GitlabSchema.types['SnippetBlob'] do
it 'has the correct fields' do
expected_fields = [:highlighted_data, :raw_path,
:size, :binary, :name, :path,
- :simple_viewer, :rich_viewer]
+ :simple_viewer, :rich_viewer,
+ :mode]
is_expected.to have_graphql_fields(*expected_fields)
end
diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
index 4ebd0977832..66ddf6c0ee6 100644
--- a/spec/javascripts/ide/components/new_dropdown/upload_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
@@ -14,7 +14,7 @@ describe('new dropdown upload', () => {
vm.entryName = 'testing';
- spyOn(vm, '$emit');
+ spyOn(vm, '$emit').and.callThrough();
});
afterEach(() => {
@@ -61,31 +61,44 @@ describe('new dropdown upload', () => {
const binaryTarget = {
result: 'base64,w4I=',
};
- const textFile = {
- name: 'textFile',
- type: 'text/plain',
- };
+ const textFile = new File(['plain text'], 'textFile');
+
const binaryFile = {
name: 'binaryFile',
type: 'image/png',
};
- it('creates file in plain text (without encoding) if the file content is plain text', () => {
+ beforeEach(() => {
+ spyOn(FileReader.prototype, 'readAsText').and.callThrough();
+ });
+
+ it('calls readAsText and creates file in plain text (without encoding) if the file content is plain text', done => {
+ const waitForCreate = new Promise(resolve => vm.$on('create', resolve));
+
vm.createFile(textTarget, textFile);
- expect(vm.$emit).toHaveBeenCalledWith('create', {
- name: textFile.name,
- type: 'blob',
- content: 'plain text',
- base64: false,
- binary: false,
- rawPath: '',
- });
+ expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(textFile);
+
+ waitForCreate
+ .then(() => {
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ name: textFile.name,
+ type: 'blob',
+ content: 'plain text',
+ base64: false,
+ binary: false,
+ rawPath: '',
+ });
+ })
+ .then(done)
+ .catch(done.fail);
});
it('splits content on base64 if binary', () => {
vm.createFile(binaryTarget, binaryFile);
+ expect(FileReader.prototype.readAsText).not.toHaveBeenCalledWith(textFile);
+
expect(vm.$emit).toHaveBeenCalledWith('create', {
name: binaryFile.name,
type: 'blob',
diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
index e3f6609f128..e2a1ed931f1 100644
--- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -58,14 +58,34 @@ describe('ContentViewer', () => {
it('renders fallback download control', done => {
createComponent({
- path: 'test.abc',
+ path: 'somepath/test.abc',
fileSize: 1024,
});
setTimeout(() => {
- expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain('test.abc');
- expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain('(1.00 KiB)');
- expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toContain('Download');
+ expect(
+ vm.$el
+ .querySelector('.file-info')
+ .textContent.trim()
+ .replace(/\s+/, ' '),
+ ).toEqual('test.abc (1.00 KiB)');
+
+ expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toEqual('Download');
+
+ done();
+ });
+ });
+
+ it('renders fallback download control for file with a data URL path properly', done => {
+ createComponent({
+ path: 'data:application/octet-stream;base64,U0VMRUNUICfEhHNnc2cnIGZyb20gVGFibGVuYW1lOwoK',
+ filePath: 'somepath/test.abc',
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.file-info').textContent.trim()).toEqual('test.abc');
+ expect(vm.$el.querySelector('.btn.btn-default')).toHaveAttr('download', 'test.abc');
+ expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toEqual('Download');
done();
});
diff --git a/spec/lib/gitlab/repository_cache_spec.rb b/spec/lib/gitlab/repository_cache_spec.rb
index 1b7dd1766da..e787288fc51 100644
--- a/spec/lib/gitlab/repository_cache_spec.rb
+++ b/spec/lib/gitlab/repository_cache_spec.rb
@@ -50,6 +50,18 @@ describe Gitlab::RepositoryCache do
end
end
+ describe '#write' do
+ it 'writes the given key and value to the cache' do
+ cache.write(:test, 'test')
+ expect(backend).to have_received(:write).with("test:#{namespace}", 'test')
+ end
+
+ it 'passes additional options to the backend' do
+ cache.write(:test, 'test', expires_in: 10.minutes)
+ expect(backend).to have_received(:write).with("test:#{namespace}", 'test', expires_in: 10.minutes)
+ end
+ end
+
describe '#fetch_without_caching_false', :use_clean_rails_memory_store_caching do
let(:key) { :foo }
let(:backend) { Rails.cache }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 19a45ce5f88..3d28adade05 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -494,6 +494,100 @@ describe Repository do
it { is_expected.to eq(commit.sha) }
end
+ describe "#merged_branch_names", :clean_gitlab_redis_cache do
+ subject { repository.merged_branch_names(branch_names) }
+
+ let(:branch_names) { %w(test beep boop definitely_merged) }
+ let(:already_merged) { Set.new(["definitely_merged"]) }
+
+ let(:merge_state_hash) do
+ {
+ "test" => false,
+ "beep" => false,
+ "boop" => false,
+ "definitely_merged" => true
+ }
+ end
+
+ let_it_be(:cache) do
+ caching_config_hash = Gitlab::Redis::Cache.params
+ ActiveSupport::Cache.lookup_store(:redis_cache_store, caching_config_hash)
+ end
+
+ let(:repository_cache) do
+ Gitlab::RepositoryCache.new(repository, backend: Rails.cache)
+ end
+
+ let(:cache_key) { repository_cache.cache_key(:merged_branch_names) }
+
+ before do
+ allow(Rails).to receive(:cache) { cache }
+ allow(repository).to receive(:cache) { repository_cache }
+ allow(repository.raw_repository).to receive(:merged_branch_names).with(branch_names).and_return(already_merged)
+ end
+
+ it { is_expected.to eq(already_merged) }
+ it { is_expected.to be_a(Set) }
+
+ context "cache is empty" do
+ before do
+ cache.delete(cache_key)
+ end
+
+ it { is_expected.to eq(already_merged) }
+
+ describe "cache values" do
+ it "writes the values to redis" do
+ expect(cache).to receive(:write).with(cache_key, merge_state_hash, expires_in: Repository::MERGED_BRANCH_NAMES_CACHE_DURATION)
+
+ subject
+ end
+
+ it "matches the supplied hash" do
+ subject
+
+ expect(cache.read(cache_key)).to eq(merge_state_hash)
+ end
+ end
+ end
+
+ context "cache is not empty" do
+ before do
+ cache.write(cache_key, merge_state_hash)
+ end
+
+ it { is_expected.to eq(already_merged) }
+
+ it "doesn't fetch from the disk" do
+ expect(repository.raw_repository).not_to receive(:merged_branch_names)
+
+ subject
+ end
+ end
+
+ context "cache is partially complete" do
+ before do
+ allow(repository.raw_repository).to receive(:merged_branch_names).with(["boop"]).and_return([])
+ hash = merge_state_hash.except("boop")
+ cache.write(cache_key, hash)
+ end
+
+ it { is_expected.to eq(already_merged) }
+
+ it "does fetch from the disk" do
+ expect(repository.raw_repository).to receive(:merged_branch_names).with(["boop"])
+
+ subject
+ end
+ end
+
+ context "requested branches array is empty" do
+ let(:branch_names) { [] }
+
+ it { is_expected.to eq(already_merged) }
+ end
+ end
+
describe '#can_be_merged?' do
context 'mergeable branches' do
subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') }
@@ -1784,6 +1878,7 @@ describe Repository do
:avatar,
:exists?,
:root_ref,
+ :merged_branch_names,
:has_visible_content?,
:issue_template_names,
:merge_request_template_names,
@@ -1959,7 +2054,7 @@ describe Repository do
describe '#expire_branches_cache' do
it 'expires the cache' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(branch_names branch_count has_visible_content?))
+ .with(%i(branch_names merged_branch_names branch_count has_visible_content?))
.and_call_original
repository.expire_branches_cache
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 99374d28324..d4deb049ea7 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -608,7 +608,7 @@ describe API::Branches do
expect(json_response['message']).to eq('Branch name is invalid')
end
- it 'returns 400 if branch already exists' do
+ it 'returns 400 if branch already exists', :clean_gitlab_redis_cache do
post api(route, user), params: { branch: 'new_design1', ref: branch_sha }
expect(response).to have_gitlab_http_status(201)