Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
55693cc1ec
commit
63a015fd85
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -51,7 +51,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-new-btn">
|
||||
<div class="ide-new-btn d-none">
|
||||
<div
|
||||
:class="{
|
||||
show: isOpen,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix some of the file encoding issues when uploading in the Web IDE
|
||||
merge_request: 23761
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add mode field to snippet blob in GraphQL
|
||||
merge_request: 24157
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Separate issue entities into own class files
|
||||
merge_request: 24226
|
||||
author: Rajendra Kadam
|
||||
type: added
|
|
@ -6641,6 +6641,11 @@ type SnippetBlob {
|
|||
"""
|
||||
highlightedData: String
|
||||
|
||||
"""
|
||||
Blob mode
|
||||
"""
|
||||
mode: String
|
||||
|
||||
"""
|
||||
Blob name
|
||||
"""
|
||||
|
|
|
@ -7171,6 +7171,20 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "mode",
|
||||
"description": "Blob mode",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Blob name",
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class ExternalIssue < Grape::Entity
|
||||
expose :title
|
||||
expose :id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -2104,6 +2104,9 @@ msgstr ""
|
|||
msgid "Approver"
|
||||
msgstr ""
|
||||
|
||||
msgid "Approvers"
|
||||
msgstr ""
|
||||
|
||||
msgid "Apr"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -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]"
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue