Commit 766b24b8 authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 1385b54a
47f676eea28871563414671e1016fb28b1b3e167
d2e978f8e8f47a49c3bcfbd470b2f790e52c5ee2
<script>
import { GlBanner } from '@gitlab/ui';
import { s__ } from '~/locale';
import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
export default {
components: {
GlBanner,
},
inject: ['svgPath', 'inviteMembersPath'],
inject: ['svgPath', 'inviteMembersPath', 'isDismissedKey'],
data() {
return {
visible: true,
isDismissed: parseBoolean(getCookie(this.isDismissedKey)),
};
},
methods: {
handleClose() {
this.visible = false;
setCookie(this.isDismissedKey, true);
this.isDismissed = true;
},
},
i18n: {
......@@ -29,7 +31,7 @@ export default {
<template>
<gl-banner
v-if="visible"
v-if="!isDismissed"
ref="banner"
:title="$options.i18n.title"
:button-text="$options.i18n.button_text"
......
......@@ -8,13 +8,14 @@ export default function initInviteMembersBanner() {
return false;
}
const { svgPath, inviteMembersPath } = el.dataset;
const { svgPath, inviteMembersPath, isDismissedKey } = el.dataset;
return new Vue({
el,
provide: {
svgPath,
inviteMembersPath,
isDismissedKey,
},
render: createElement => createElement(InviteMembersBanner),
});
......
......@@ -68,7 +68,7 @@ export default {
return templatedContent;
},
onInputChange(newVal) {
this.parsedSource.sync(newVal, this.isWysiwygMode);
this.parsedSource.syncContent(newVal, this.isWysiwygMode);
this.isModified = this.parsedSource.isModified();
},
onModeChange(mode) {
......
import getFrontMatterLanguageDefinition from './parse_source_file_language_support';
import grayMatter from 'gray-matter';
const parseSourceFile = (raw, options = { frontMatterLanguage: 'yaml' }) => {
const { open, close } = getFrontMatterLanguageDefinition(options.frontMatterLanguage);
const anyChar = '[\\s\\S]';
const frontMatterBlock = `^${open}$${anyChar}*?^${close}$`;
const frontMatterRegex = new RegExp(`${frontMatterBlock}`, 'm');
const preGroupedRegex = new RegExp(`(${anyChar}*?)(${frontMatterBlock})(\\s*)(${anyChar}*)`, 'm'); // preFrontMatter, frontMatter, spacing, and content
let initial;
let editable;
const parseSourceFile = raw => {
const remake = source => grayMatter(source, {});
const hasFrontMatter = source => frontMatterRegex.test(source);
let editable = remake(raw);
const buildPayload = (source, header, spacing, body) => {
return { raw: source, header, spacing, body };
};
const parse = source => {
if (hasFrontMatter(source)) {
const match = source.match(preGroupedRegex);
const [, preFrontMatter, frontMatter, spacing, content] = match;
const header = preFrontMatter + frontMatter;
return buildPayload(source, header, spacing, content);
const syncContent = (newVal, isBody) => {
if (isBody) {
editable.content = newVal;
} else {
editable = remake(newVal);
}
return buildPayload(source, '', '', source);
};
const syncEditable = () => {
/*
We re-parse as markdown editing could have added non-body changes (preFrontMatter, frontMatter, or spacing).
Re-parsing additionally gets us the desired body that was extracted from the potentially mutated editable.raw
*/
editable = parse(editable.raw);
};
const trimmedEditable = () => grayMatter.stringify(editable).trim();
const refreshEditableRaw = () => {
editable.raw = `${editable.header}${editable.spacing}${editable.body}`;
};
const content = (isBody = false) => (isBody ? editable.content.trim() : trimmedEditable()); // gray-matter internally adds an eof newline so we trim to bypass, open issue: https://github.com/jonschlinkert/gray-matter/issues/96
const sync = (newVal, isBodyToRaw) => {
const editableKey = isBodyToRaw ? 'body' : 'raw';
editable[editableKey] = newVal;
const matter = () => editable.matter;
if (isBodyToRaw) {
refreshEditableRaw();
}
syncEditable();
const syncMatter = newMatter => {
const targetMatter = newMatter.replace(/---/gm, ''); // TODO dynamic delimiter removal vs. hard code
const currentMatter = matter();
const currentContent = content();
const newSource = currentContent.replace(currentMatter, targetMatter);
syncContent(newSource);
editable.matter = newMatter;
};
const frontMatter = () => editable.header;
const matterObject = () => editable.data;
const setFrontMatter = val => {
editable.header = val;
refreshEditableRaw();
const syncMatterObject = obj => {
editable.data = obj;
};
const content = (isBody = false) => {
const editableKey = isBody ? 'body' : 'raw';
return editable[editableKey];
};
const isModified = () => initial.raw !== editable.raw;
initial = parse(raw);
editable = parse(raw);
const isModified = () => trimmedEditable() !== raw;
return {
frontMatter,
setFrontMatter,
matter,
syncMatter,
matterObject,
syncMatterObject,
content,
syncContent,
isModified,
sync,
};
};
......
const frontMatterLanguageDefinitions = [
{ name: 'yaml', open: '---', close: '---' },
{ name: 'toml', open: '\\+\\+\\+', close: '\\+\\+\\+' },
{ name: 'json', open: '{', close: '}' },
];
const getFrontMatterLanguageDefinition = name => {
const languageDefinition = frontMatterLanguageDefinitions.find(def => def.name === name);
if (!languageDefinition) {
throw new Error(`Unsupported front matter language: ${name}`);
}
return languageDefinition;
};
export default getFrontMatterLanguageDefinition;
......@@ -45,8 +45,8 @@ export default class MergeRequestStore {
this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path;
this.mergeStatus = data.merge_status;
this.commitMessage = data.default_merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
this.mergeCommitSha = data.merge_commit_sha;
this.shortMergeCommitSha = data.short_merged_commit_sha;
this.mergeCommitSha = data.merged_commit_sha;
this.commitMessageWithDescription = data.default_merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
......@@ -135,7 +135,7 @@ export default class MergeRequestStore {
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergePath = data.merge_path;
this.canMerge = Boolean(data.merge_path);
this.mergeCommitPath = data.merge_commit_path;
this.mergeCommitPath = data.merged_commit_path;
this.canPushToSourceBranch = data.can_push_to_source_branch;
if (data.work_in_progress !== undefined) {
......
......@@ -64,7 +64,7 @@ def render_diffs
render: ->(partial, locals) { view_to_html_string(partial, locals) }
}
options = additional_attributes.merge(diff_view: diff_view)
options = additional_attributes.merge(diff_view: Feature.enabled?(:unified_diff_lines, @merge_request.project) ? "inline" : diff_view)
if @merge_request.project.context_commits_enabled?
options[:context_commits] = @merge_request.recent_context_commits
......
......@@ -428,7 +428,13 @@ def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42438')
end
def reports_response(report_comparison)
def reports_response(report_comparison, pipeline = nil)
if pipeline&.active?
::Gitlab::PollingInterval.set_header(response, interval: 3000)
render json: '', status: :no_content && return
end
case report_comparison[:status]
when :parsing
::Gitlab::PollingInterval.set_header(response, interval: 3000)
......
......@@ -29,11 +29,11 @@ class IssueMoveList < Mutations::Issues::Base
argument :move_before_id, GraphQL::ID_TYPE,
required: false,
description: 'ID of issue before which the current issue will be positioned at'
description: 'ID of issue that should be placed before the current issue'
argument :move_after_id, GraphQL::ID_TYPE,
required: false,
description: 'ID of issue after which the current issue will be positioned at'
description: 'ID of issue that should be placed after the current issue'
def ready?(**args)
if move_arguments(args).blank?
......
# frozen_string_literal: true
module Resolvers
module Admin
module Analytics
module InstanceStatistics
class MeasurementsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Admin::Analytics::InstanceStatistics::MeasurementType, null: true
argument :identifier, Types::Admin::Analytics::InstanceStatistics::MeasurementIdentifierEnum,
required: true,
description: 'The type of measurement/statistics to retrieve'
def resolve(identifier:)
authorize!
::Analytics::InstanceStatistics::Measurement
.with_identifier(identifier)
.order_by_latest
end
private
def authorize!
admin? || raise_resource_not_available_error!
end
def admin?
context[:current_user].present? && context[:current_user].admin?
end
end
end
end
end
end
# frozen_string_literal: true
module Types
module Admin
module Analytics
module InstanceStatistics
class MeasurementIdentifierEnum < BaseEnum
graphql_name 'MeasurementIdentifier'
description 'Possible identifier types for a measurement'
value 'PROJECTS', 'Project count', value: :projects
value 'USERS', 'User count', value: :users
value 'ISSUES', 'Issue count', value: :issues
value 'MERGE_REQUESTS', 'Merge request count', value: :merge_requests
value 'GROUPS', 'Group count', value: :groups
value 'PIPELINES', 'Pipeline count', value: :pipelines
end
end
end
end
end
# frozen_string_literal: true
# rubocop:disable Graphql/AuthorizeTypes
module Types
module Admin
module Analytics
module InstanceStatistics
class MeasurementType < BaseObject
graphql_name 'InstanceStatisticsMeasurement'
description 'Represents a recorded measurement (object count) for the Admins'
field :recorded_at, Types::TimeType, null: true,
description: 'The time the measurement was recorded'
field :count, GraphQL::INT_TYPE, null: false,
description: 'Object count'
field :identifier, Types::Admin::Analytics::InstanceStatistics::MeasurementIdentifierEnum, null: false,
description: 'The type of objects being measured'
end
end
end
end
end
......@@ -76,6 +76,11 @@ class QueryType < ::Types::BaseObject
argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the Issue'
end
field :instance_statistics_measurements, Types::Admin::Analytics::InstanceStatistics::MeasurementType.connection_type,
null: true,
description: 'Get statistics on the instance',
resolver: Resolvers::Admin::Analytics::InstanceStatistics::MeasurementsResolver
def design_management
DesignManagementObject.new(nil)
end
......
......@@ -3,10 +3,20 @@
module Analytics
module InstanceStatistics
class Measurement < ApplicationRecord
enum identifier: { projects: 1, users: 2 }
enum identifier: {
projects: 1,
users: 2,
issues: 3,
merge_requests: 4,
groups: 5,
pipelines: 6
}
validates :recorded_at, :identifier, :count, presence: true
validates :recorded_at, uniqueness: { scope: :identifier }
scope :order_by_latest, -> { order(recorded_at: :desc) }
scope :with_identifier, -> (identifier) { where(identifier: identifier) }
end
end
end
......@@ -1473,6 +1473,19 @@ def short_merge_commit_sha
Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
end
def merged_commit_sha
return unless merged?
sha = merge_commit_sha || squash_commit_sha || diff_head_sha
sha.presence
end
def short_merged_commit_sha
if sha = merged_commit_sha
Commit.truncate_sha(sha)
end
end
def can_be_reverted?(current_user)
return false unless merge_commit
return false unless merged_at
......
......@@ -3,8 +3,8 @@
class MergeRequestPollCachedWidgetEntity < IssuableEntity
expose :auto_merge_enabled
expose :state
expose :merge_commit_sha
expose :short_merge_commit_sha
expose :merged_commit_sha
expose :short_merged_commit_sha
expose :merge_error
expose :public_merge_status, as: :merge_status
expose :merge_user_id
......@@ -56,9 +56,9 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
presenter(merge_request).target_branch_tree_path
end
expose :merge_commit_path do |merge_request|
if merge_request.merge_commit_sha
project_commit_path(merge_request.project, merge_request.merge_commit_sha)
expose :merged_commit_path do |merge_request|
if sha = merge_request.merged_commit_sha
project_commit_path(merge_request.project, sha)
end
end
......
......@@ -3,7 +3,7 @@
%p #{@service.description} template.
= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form|
= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form js-integration-settings-form' } do |form|
= render 'shared/service_settings', form: form, integration: @service
.footer-block.row-content-block
......
......@@ -6,6 +6,7 @@
= content_for :group_invite_members_banner do
.container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
.js-group-invite-members-banner{ data: { svg_path: image_path('illustrations/merge_requests.svg'),
is_dismissed_key: "invite_#{@group.id}_#{current_user.id}",
invite_members_path: group_group_members_path(@group) } }
= content_for :meta_tags do
......
---
title: Resolve Fix validation on External Wiki service template form
merge_request: 41964
author:
type: fixed
---
title: Expose Instance Statistics measurements (object counts) via GraphQL
merge_request: 40871
author:
type: added
---
title: Refuse to perform an LFS clean on projects that are fork roots
merge_request: 41703
author:
type: fixed
---
title: Display merged commit sha in fast-forward merge mode
merge_request: 41369
author: Mycroft Kang @TaehyeokKang
type: added
---
title: Ensure namespace settings are backfilled via migration
merge_request: 41679
author:
type: other
---
name: usage_data_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41301
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235459
group: group::telemetry
type: development
default_enabled: false
# frozen_string_literal: true
class CompleteNamespaceSettingsMigration < ActiveRecord::Migration[5.2]
DOWNTIME = false
BATCH_SIZE = 10000
class Namespace < ActiveRecord::Base
include EachBatch
self.table_name = 'namespaces'
end
def up
Gitlab::BackgroundMigration.steal('BackfillNamespaceSettings')
ensure_data_migration
end
def down
# no-op
end
private
def ensure_data_migration
Namespace.each_batch(of: BATCH_SIZE) do |query|
missing_count = query.where("NOT EXISTS (SELECT 1 FROM namespace_settings WHERE namespace_settings.namespace_id=namespaces.id)").limit(1).size
if missing_count > 0
min, max = query.pluck("MIN(id), MAX(id)").flatten
# we expect low record count so inline execution is fine.
Gitlab::BackgroundMigration::BackfillNamespaceSettings.new.perform(min, max)
end
end
end
end
2311967a9f68e1a428662e0231752ad0d844063d66cca895211d38f9ae928d94
\ No newline at end of file
......@@ -76,6 +76,7 @@ Citus
clonable
Cloudwatch
Cobertura
Codepen
Cognito
colocated
colocating
......
......@@ -455,7 +455,7 @@ POST /projects/:id/boards/:board_id/lists
NOTE: **Note:**
Label, assignee and milestone arguments are mutually exclusive,
that is, only one of them are accepted in a request.
Check the [Issue Board docs](../user/project/issue_board.md#summary-of-features-per-tier)
Check the [Issue Board docs](../user/project/issue_board.md)
for more information regarding the required license for each list type.
```shell
......
......@@ -7410,6 +7410,61 @@ type InstanceSecurityDashboard {
): VulnerabilitySeveritiesCount
}