Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-04-15 12:09:18 +00:00
parent 221b529789
commit b7c735c8ac
71 changed files with 799 additions and 141 deletions

View file

@ -0,0 +1,53 @@
<script>
import eventHub from '../event_hub';
import { GlToggle } from '@gitlab/ui';
export default {
name: 'ActiveToggle',
components: {
GlToggle,
},
props: {
initialActivated: {
type: Boolean,
required: true,
},
disabled: {
type: Boolean,
required: true,
},
},
data() {
return {
activated: this.initialActivated,
};
},
mounted() {
// Initialize view
this.$nextTick(() => {
this.onToggle(this.activated);
});
},
methods: {
onToggle(e) {
eventHub.$emit('toggle', e);
},
},
};
</script>
<template>
<div>
<div class="form-group row" role="group">
<label for="service[active]" class="col-form-label col-sm-2">{{ __('Active') }}</label>
<div class="col-sm-10 pt-1">
<gl-toggle
v-model="activated"
:disabled="disabled"
name="service[active]"
@change="onToggle"
/>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,3 @@
import Vue from 'vue';
export default new Vue();

View file

@ -0,0 +1,30 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import ActiveToggle from './components/active_toggle.vue';
export default el => {
if (!el) {
return null;
}
const { showActive: showActiveStr, activated: activatedStr, disabled: disabledStr } = el.dataset;
const showActive = parseBoolean(showActiveStr);
const activated = parseBoolean(activatedStr);
const disabled = parseBoolean(disabledStr);
if (!showActive) {
return null;
}
return new Vue({
el,
render(createElement) {
return createElement(ActiveToggle, {
props: {
initialActivated: activated,
disabled,
},
});
},
});
};

View file

@ -2,28 +2,33 @@ import $ from 'jquery';
import axios from '../lib/utils/axios_utils';
import flash from '../flash';
import { __ } from '~/locale';
import initForm from './edit';
import eventHub from './edit/event_hub';
export default class IntegrationSettingsForm {
constructor(formSelector) {
this.$form = $(formSelector);
this.formActive = false;
// Form Metadata
this.canTestService = this.$form.data('canTest');
this.testEndPoint = this.$form.data('testUrl');
// Form Child Elements
this.$serviceToggle = this.$form.find('#service_active');
this.$submitBtn = this.$form.find('button[type="submit"]');
this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner');
this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label');
}
init() {
// Initialize View
this.toggleServiceState(this.$serviceToggle.is(':checked'));
// Init Vue component
initForm(document.querySelector('.js-vue-integration-settings'));
eventHub.$on('toggle', active => {
this.formActive = active;
this.handleServiceToggle();
});
// Bind Event Listeners
this.$serviceToggle.on('change', e => this.handleServiceToggle(e));
this.$submitBtn.on('click', e => this.handleSettingsSave(e));
}
@ -31,7 +36,7 @@ export default class IntegrationSettingsForm {
// Check if Service is marked active, as if not marked active,
// We can skip testing it and directly go ahead to allow form to
// be submitted
if (!this.$serviceToggle.is(':checked')) {
if (!this.formActive) {
return;
}
@ -47,16 +52,16 @@ export default class IntegrationSettingsForm {
}
}
handleServiceToggle(e) {
this.toggleServiceState($(e.currentTarget).is(':checked'));
handleServiceToggle() {
this.toggleServiceState();
}
/**
* Change Form's validation enforcement based on service status (active/inactive)
*/
toggleServiceState(serviceActive) {
this.toggleSubmitBtnLabel(serviceActive);
if (serviceActive) {
toggleServiceState() {
this.toggleSubmitBtnLabel();
if (this.formActive) {
this.$form.removeAttr('novalidate');
} else if (!this.$form.attr('novalidate')) {
this.$form.attr('novalidate', 'novalidate');
@ -66,10 +71,10 @@ export default class IntegrationSettingsForm {
/**
* Toggle Submit button label based on Integration status and ability to test service
*/
toggleSubmitBtnLabel(serviceActive) {
toggleSubmitBtnLabel() {
let btnLabel = __('Save changes');
if (serviceActive && this.canTestService) {
if (this.formActive && this.canTestService) {
btnLabel = __('Test settings and save changes');
}

View file

@ -39,13 +39,18 @@ export default {
required: false,
default: true,
},
showSpinner: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
toggleChevronClass() {
return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down';
},
noteTimestampLink() {
return `#note_${this.noteId}`;
return this.noteId ? `#note_${this.noteId}` : undefined;
},
hasAuthor() {
return this.author && Object.keys(this.author).length;
@ -60,7 +65,9 @@ export default {
this.$emit('toggleHandler');
},
updateTargetNoteHash() {
this.setTargetNoteHash(this.noteTimestampLink);
if (this.$store) {
this.setTargetNoteHash(this.noteTimestampLink);
}
},
},
};
@ -101,16 +108,20 @@ export default {
<template v-if="actionText">{{ actionText }}</template>
</span>
<a
ref="noteTimestamp"
v-if="noteTimestampLink"
ref="noteTimestampLink"
:href="noteTimestampLink"
class="note-timestamp system-note-separator"
@click="updateTargetNoteHash"
>
<time-ago-tooltip :time="createdAt" tooltip-placement="bottom" />
</a>
<time-ago-tooltip v-else ref="noteTimestamp" :time="createdAt" tooltip-placement="bottom" />
</template>
<slot name="extra-controls"></slot>
<i
v-if="showSpinner"
ref="spinner"
class="fa fa-spinner fa-spin editing-spinner"
:aria-label="__('Comment is being updated')"
aria-hidden="true"

View file

@ -9,7 +9,7 @@ class Projects::ReleasesController < Projects::ApplicationController
push_frontend_feature_flag(:release_issue_summary, project, default_enabled: true)
push_frontend_feature_flag(:release_evidence_collection, project, default_enabled: true)
push_frontend_feature_flag(:release_show_page, project, default_enabled: true)
push_frontend_feature_flag(:release_asset_link_editing, project)
push_frontend_feature_flag(:release_asset_link_editing, project, default_enabled: true)
end
before_action :authorize_update_release!, only: %i[edit update]

View file

@ -160,4 +160,14 @@ module SnippetsHelper
title: 'Download',
rel: 'noopener noreferrer')
end
def snippet_file_name(snippet)
blob = if Feature.enabled?(:version_snippets, current_user) && !snippet.repository.empty?
snippet.blobs.first
else
snippet.blob
end
blob.name
end
end

View file

@ -28,9 +28,20 @@ class DiffNotePosition < ApplicationRecord
end
def position=(position)
assign_attributes(self.class.position_to_attrs(position))
end
def self.create_or_update_for(note, params)
attrs = position_to_attrs(params[:position])
attrs.merge!(params.slice(:diff_type, :line_code))
attrs[:note_id] = note.id
upsert(attrs, unique_by: [:note_id, :diff_type])
end
def self.position_to_attrs(position)
position_attrs = position.to_h
position_attrs[:diff_content_type] = position_attrs.delete(:position_type)
assign_attributes(position_attrs)
position_attrs
end
end

View file

@ -83,6 +83,7 @@ class Note < ApplicationRecord
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata
has_one :note_diff_file, inverse_of: :diff_note, foreign_key: :diff_note_id
has_many :diff_note_positions
delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true

View file

@ -4,7 +4,7 @@ module PerformanceMonitoring
class PrometheusPanel
include ActiveModel::Model
attr_accessor :type, :title, :y_label, :weight, :metrics, :y_axis
attr_accessor :type, :title, :y_label, :weight, :metrics, :y_axis, :max_value
validates :title, presence: true
validates :metrics, presence: true

View file

@ -0,0 +1,65 @@
# frozen_string_literal: true
module Discussions
class CaptureDiffNotePositionService
def initialize(merge_request, paths)
@project = merge_request.project
@tracer = build_tracer(merge_request, paths)
end
def execute(discussion)
# The service has been implemented for text only
# The impact of image notes on this service is being investigated in
# https://gitlab.com/gitlab-org/gitlab/-/issues/213989
return unless discussion.on_text?
result = tracer&.trace(discussion.position)
return unless result
position = result[:position]
# Currently position data is copied across all notes of a discussion
# It makes sense to store a position only for the first note instead
# Within the newly introduced table we can start doing just that
DiffNotePosition.create_or_update_for(discussion.notes.first,
diff_type: :head,
position: position,
line_code: position.line_code(project.repository))
end
private
attr_reader :tracer, :project
def build_tracer(merge_request, paths)
return if paths.blank?
old_diff_refs, new_diff_refs = build_diff_refs(merge_request)
return unless old_diff_refs && new_diff_refs
Gitlab::Diff::PositionTracer.new(
project: project,
old_diff_refs: old_diff_refs,
new_diff_refs: new_diff_refs,
paths: paths.uniq)
end
def build_diff_refs(merge_request)
merge_ref_head = merge_request.merge_ref_head
return unless merge_ref_head
start_sha, base_sha = merge_ref_head.parent_ids
new_diff_refs = Gitlab::Diff::DiffRefs.new(
base_sha: base_sha,
start_sha: start_sha,
head_sha: merge_ref_head.id)
old_diff_refs = merge_request.diff_refs
return if new_diff_refs == old_diff_refs
[old_diff_refs, new_diff_refs]
end
end
end

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Discussions
class CaptureDiffNotePositionsService
def initialize(merge_request)
@merge_request = merge_request
end
def execute
return unless merge_request.has_complete_diff_refs?
discussions, paths = build_discussions
service = Discussions::CaptureDiffNotePositionService.new(merge_request, paths)
discussions.each do |discussion|
service.execute(discussion)
end
end
private
attr_reader :merge_request
def build_discussions
active_diff_discussions = merge_request.notes.new_diff_notes.discussions.select do |discussion|
discussion.active?(merge_request.diff_refs)
end
paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }
[active_diff_discussions, paths]
end
end
end

View file

@ -118,11 +118,18 @@ module MergeRequests
if can_git_merge? && merge_to_ref
merge_request.mark_as_mergeable
update_diff_discussion_positions!
else
merge_request.mark_as_unmergeable
end
end
def update_diff_discussion_positions!
return if Feature.disabled?(:merge_ref_head_comments, merge_request.target_project)
Discussions::CaptureDiffNotePositionsService.new(merge_request).execute
end
def recheck!
if !merge_request.recheck_merge_status? && outdated_merge_ref?
merge_request.mark_as_unchecked

View file

@ -65,6 +65,10 @@ module Notes
if Feature.enabled?(:notes_create_service_tracking, project)
Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note))
end
if Feature.enabled?(:merge_ref_head_comments, project) && note.for_merge_request? && note.diff_note? && note.start_of_discussion?
Discussions::CaptureDiffNotePositionService.new(note.noteable, note.diff_file&.paths).execute(note.discussion)
end
end
def do_commands(note, update_params, message, only_commands)

View file

@ -37,6 +37,7 @@
= render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/subgroup_creation_level', f: f, group: @group
= render 'groups/settings/two_factor_auth', f: f
= render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group
= render_if_exists 'groups/member_lock_setting', f: f, group: @group
= f.submit _('Save changes'), class: 'btn btn-success prepend-top-default js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' }

View file

@ -8,11 +8,7 @@
= markdown @service.help
.service-settings
- if @service.show_active_box?
.form-group.row
= form.label :active, "Active", class: "col-form-label col-sm-2"
.col-sm-10
= form.check_box :active, checked: @service.active || @service.new_record?, disabled: disable_fields_service?(@service)
.js-vue-integration-settings{ data: { show_active: @service.show_active_box?.to_s, activated: (@service.active || @service.new_record?).to_s, disabled: disable_fields_service?(@service).to_s } }
- if @service.configurable_events.present?
.form-group.row

View file

@ -1,5 +1,6 @@
- link_project = local_assigns.fetch(:link_project, false)
- notes_count = @noteable_meta_data[snippet.id].user_notes_count
- file_name = snippet_file_name(snippet)
%li.snippet-row.py-3
= image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
@ -7,10 +8,10 @@
.title
= link_to gitlab_snippet_path(snippet) do
= snippet.title
- if snippet.file_name.present?
- if file_name.present?
%span.snippet-filename.d-none.d-sm-inline-block.ml-2
= sprite_icon('doc-code', size: 16, css_class: 'file-icon align-text-bottom')
= snippet.file_name
= file_name
%ul.controls
%li

View file

@ -944,7 +944,7 @@
- :name: background_migration
:feature_category: :database
:has_external_dependencies:
:urgency: :low
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
:idempotent:

View file

@ -4,6 +4,7 @@ class BackgroundMigrationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
feature_category :database
urgency :throttled
# The minimum amount of time between processing two jobs of the same migration
# class.

View file

@ -0,0 +1,5 @@
---
title: Update Active checkbox component to use toggle
merge_request: 27778
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Deprecate 'token' attribute from Runners API
merge_request: 29481
author:
type: deprecated

View file

@ -0,0 +1,6 @@
---
title: Fix dashboard processing error which prevented dashboards with unknown attributes
inside panels from being displayed
merge_request: 29517
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Migrate legacy uploads out of deprecated paths
merge_request: 29295
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Allow Release links to be edited on the Edit Release page
merge_request: 28816
author:
type: added

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
class MigrateLegacyAttachments < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
MIGRATION = 'LegacyUploadsMigrator'
BATCH_SIZE = 5000
INTERVAL = 5.minutes.to_i
class Upload < ActiveRecord::Base
self.table_name = 'uploads'
include ::EachBatch
end
def up
queue_background_migration_jobs_by_range_at_intervals(Upload.where(uploader: 'AttachmentUploader', model_type: 'Note'),
MIGRATION,
INTERVAL,
batch_size: BATCH_SIZE)
end
def down
# no-op
end
end

View file

@ -13152,5 +13152,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200408110856
20200408153842
20200408175424
20200409211607
\.

View file

@ -111,24 +111,6 @@ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, MergeReque
sudo -u git -H bundle exec rake "gitlab:uploads:migrate[DesignManagement::DesignV432x230Uploader, DesignManagement::Action]"
```
## Migrate legacy uploads out of deprecated paths
> Introduced in GitLab 12.3.
To migrate all uploads created by legacy uploaders, run:
**Omnibus Installation**
```shell
gitlab-rake gitlab:uploads:legacy:migrate
```
**Source Installation**
```shell
bundle exec rake gitlab:uploads:legacy:migrate
```
## Migrate from object storage to local storage
If you need to disable Object Storage for any reason, first you need to migrate

View file

@ -162,6 +162,10 @@ GET /runners/:id
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6"
```
CAUTION: **Deprecation**
The `token` attribute in the response is deprecated [since GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/214320).
It will be removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/214322).
Example response:
```json
@ -221,6 +225,10 @@ PUT /runners/:id
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
```
CAUTION: **Deprecation**
The `token` attribute in the response is deprecated [since GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/214320).
It will be removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/214322).
Example response:
```json

View file

@ -112,6 +112,32 @@ To access the Credentials inventory of a group, navigate to **{shield}** **Secur
This feature is similar to the [Credentials inventory for self-managed instances](../../admin_area/credentials_inventory.md).
##### Limiting lifetime of personal access tokens of users in Group-managed accounts **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118893) in GitLab 12.10.
Users in a group managed account can optionally specify an expiration date for
[personal access tokens](../../profile/personal_access_tokens.md).
This expiration date is not a requirement, and can be set to any arbitrary date.
Since personal access tokens are the only token needed for programmatic access to GitLab, organizations with security requirements may want to enforce more protection to require regular rotation of these tokens.
###### Setting a limit
Only a GitLab administrator or an owner of a Group-managed account can set a limit. Leaving it empty means that the [instance level restrictions](../../admin_area/settings/account_and_limit_settings.md#limiting-lifetime-of-personal-access-tokens-ultimate-only) on the lifetime of personal access tokens will apply.
To set a limit on how long personal access tokens are valid for users in a group managed account:
1. Navigate to the **{settings}** **Settings > General** page in your group's sidebar.
1. Expand the **Permissions, LFS, 2FA** section.
1. Fill in the **Maximum allowable lifetime for personal access tokens (days)** field.
1. Click **Save changes**.
Once a lifetime for personal access tokens is set, GitLab will:
- Apply the lifetime for new personal access tokens, and require users managed by the group to set an expiration date that is no later than the allowed lifetime.
- After three hours, revoke old tokens with no expiration date or with a lifetime longer than the allowed lifetime. Three hours is given to allow administrators/group owner to change the allowed lifetime, or remove it, before revocation takes place.
##### Outer forks restriction for Group-managed accounts
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34648) in GitLab 12.9.

View file

@ -39,7 +39,7 @@ service in GitLab.
1. Navigate to the project you want to configure to trigger builds.
1. Navigate to the [Integrations page](overview.md#accessing-integrations)
1. Click 'Atlassian Bamboo CI'
1. Select the 'Active' checkbox.
1. Ensure that the **Active** toggle is enabled.
1. Enter the base URL of your Bamboo server. `https://bamboo.example.com`
1. Enter the build key from your Bamboo build plan. Build keys are typically made
up from the Project Key and Plan Key that are set on project/plan creation and

View file

@ -21,7 +21,7 @@ With the webhook URL created in the Discord channel, you can set up the Discord
1. Navigate to the [Integrations page](overview.md#accessing-integrations) in your project's settings. That is, **Project > Settings > Integrations**.
1. Select the **Discord Notifications** integration to configure it.
1. Check the **Active** checkbox to turn on the service.
1. Ensure that the **Active** toggle is enabled.
1. Check the checkboxes corresponding to the GitLab events for which you want to send notifications to Discord.
1. Paste the webhook URL that you copied from the create Discord webhook step.
1. Configure the remaining options and click the **Save changes** button.

View file

@ -18,7 +18,7 @@ To set up the generic alerts integration:
1. Navigate to **Settings > Integrations** in a project.
1. Click on **Alerts endpoint**.
1. Toggle the **Active** alert setting. The `URL` and `Authorization Key` for the webhook configuration can be found there.
1. Toggle the **Active** alert setting. The `URL` and `Authorization Key` for the webhook configuration can be found there.
## Customizing the payload

View file

@ -27,7 +27,7 @@ with `repo:status` access granted:
1. Navigate to the project you want to configure.
1. Navigate to the [Integrations page](overview.md#accessing-integrations)
1. Click "GitHub".
1. Select the "Active" checkbox.
1. Ensure that the **Active** toggle is enabled.
1. Paste the token you've generated on GitHub
1. Enter the path to your project on GitHub, such as `https://github.com/username/repository`
1. Optionally uncheck **Static status check names** checkbox to disable static status check names.

View file

@ -19,7 +19,7 @@ When you have the **Webhook URL** for your Hangouts Chat room webhook, you can s
1. Navigate to the [Integrations page](overview.md#accessing-integrations) in your project's settings, i.e. **Project > Settings > Integrations**.
1. Select the **Hangouts Chat** integration to configure it.
1. Check the **Active** checkbox to turn on the service.
1. Ensure that the **Active** toggle is enabled.
1. Check the checkboxes corresponding to the GitLab events you want to receive.
1. Paste the **Webhook URL** that you copied from the Hangouts Chat configuration step.
1. Configure the remaining options and click `Save changes`.

View file

@ -37,7 +37,7 @@ service in GitLab.
1. Navigate to the project you want to configure for notifications.
1. Navigate to the [Integrations page](overview.md#accessing-integrations)
1. Click "HipChat".
1. Select the "Active" checkbox.
1. Ensure that the **Active** toggle is enabled.
1. Insert the `token` field from the URL into the `Token` field on the Web page.
1. Insert the `room` field from the URL into the `Room` field on the Web page.
1. Save or optionally click "Test Settings".

View file

@ -28,7 +28,7 @@ need to follow the firsts steps of the next section.
1. Navigate to the project you want to configure for notifications.
1. Navigate to the [Integrations page](overview.md#accessing-integrations)
1. Click "Irker".
1. Select the "Active" checkbox.
1. Ensure that the **Active** toggle is enabled.
1. Enter the server host address where `irkerd` runs (defaults to `localhost`)
in the `Server host` field on the Web page
1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the

View file

@ -103,7 +103,7 @@ in a new slash command.
### Step 4. Copy the Mattermost token into the Mattermost slash command service
1. In GitLab, paste the Mattermost token you copied in the previous step and
check the **Active** checkbox.
ensure that the **Active** toggle is enabled.
![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png)

View file

@ -14,7 +14,7 @@ The Slack Notifications Service allows your GitLab project to send events (e.g.
1. Navigate to the [Integrations page](overview.md#accessing-integrations) in your project's settings, i.e. **Project > Settings > Integrations**.
1. Select the **Slack notifications** integration to configure it.
1. Check the **Active** checkbox to turn on the service.
1. Ensure that the **Active** toggle is enabled.
1. Check the checkboxes corresponding to the GitLab events you want to send to Slack as a notification.
1. For each event, optionally enter the Slack channel names where you want to send the event, separated by a comma. If left empty, the event will be sent to the default channel that you configured in the Slack Configuration step. **Note:** Usernames and private channels are not supported. To send direct messages, use the Member ID found under user's Slack profile.
1. Paste the **Webhook URL** that you copied from the Slack Configuration step.

View file

@ -19,7 +19,7 @@ For GitLab.com, use the [Slack app](gitlab_slack_application.md) instead.
1. Enter a trigger term. We suggest you use the project name. Click **Add Slash Command Integration**.
1. Complete the rest of the fields in the Slack configuration page using information from the GitLab browser tab. In particular, the URL needs to be copied and pasted. Click **Save Integration** to complete the configuration in Slack.
1. While still on the Slack configuration page, copy the **token**. Go back to the GitLab browser tab and paste in the **token**.
1. Check the **Active** checkbox and click **Save changes** to complete the configuration in GitLab.
1. Ensure that the **Active** toggle is enabled and click **Save changes** to complete the configuration in GitLab.
![Slack setup instructions](img/slack_setup.png)

View file

@ -17,7 +17,7 @@ When you have the **Webhook URL** for your Unify Circuit conversation webhook, y
1. Navigate to the [Integrations page](overview.md#accessing-integrations) in your project's settings, i.e. **Project > Settings > Integrations**.
1. Select the **Unify Circuit** integration to configure it.
1. Check the **Active** checkbox to turn on the service.
1. Ensure that the **Active** toggle is enabled.
1. Check the checkboxes corresponding to the GitLab events you want to receive in Unify Circuit.
1. Paste the **Webhook URL** that you copied from the Unify Circuit configuration step.
1. Configure the remaining options and click `Save changes`.

View file

@ -309,12 +309,12 @@ Here is an example of a Release Evidence object:
### Enabling Release Evidence display **(CORE ONLY)**
This feature comes with the `:release_evidence_collection` feature flag
disabled by default in GitLab self-managed instances. To turn it on,
enabled by default in GitLab self-managed instances. To turn it off,
ask a GitLab administrator with Rails console access to run the following
command:
```ruby
Feature.enable(:release_evidence_collection)
Feature.disable(:release_evidence_collection)
```
NOTE: **Note:**

View file

@ -10,7 +10,11 @@ module API
expose :access_level
expose :version, :revision, :platform, :architecture
expose :contacted_at
# @deprecated in 12.10 https://gitlab.com/gitlab-org/gitlab/-/issues/214320
# will be removed by 13.0 https://gitlab.com/gitlab-org/gitlab/-/issues/214322
expose :token, if: lambda { |runner, options| options[:current_user].admin? || !runner.instance_type? }
# rubocop: disable CodeReuse/ActiveRecord
expose :projects, with: Entities::BasicProjectDetails do |runner, options|
if options[:current_user].admin?

View file

@ -70,8 +70,7 @@ module Gitlab
# Do not create relation if it is:
# - An unknown service
# - A legacy trigger
unknown_service? ||
(!Feature.enabled?(:use_legacy_pipeline_triggers, @importable) && legacy_trigger?)
unknown_service? || legacy_trigger?
end
def setup_models

View file

@ -15,6 +15,9 @@ module Gitlab
insert_panel_id(id, panel)
end
rescue ActiveModel::UnknownAttributeError => error
remove_panel_ids!
Gitlab::ErrorTracking.log_exception(error)
end
private

View file

@ -1,27 +0,0 @@
# frozen_string_literal: true
namespace :gitlab do
namespace :uploads do
namespace :legacy do
desc "GitLab | Uploads | Migrate all legacy attachments"
task migrate: :environment do
class Upload < ApplicationRecord
self.table_name = 'uploads'
include ::EachBatch
end
migration = 'LegacyUploadsMigrator'
batch_size = 5000
delay_interval = 5.minutes.to_i
Upload.where(uploader: 'AttachmentUploader', model_type: 'Note').each_batch(of: batch_size) do |relation, index|
start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
delay = index * delay_interval
BackgroundMigrationWorker.perform_in(delay, migration, [start_id, end_id])
end
end
end
end
end

View file

@ -385,6 +385,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
msgid "%{no_of_days} day"
msgid_plural "%{no_of_days} days"
msgstr[0] ""
msgstr[1] ""
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@ -10840,6 +10845,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
msgstr ""
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@ -23015,6 +23023,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@ -25030,6 +25047,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
msgid "no expiration"
msgstr ""
msgid "no one can merge"
msgstr ""
@ -25315,6 +25335,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
msgid "vulnerability|Add a comment"
msgstr ""
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""

View file

@ -489,7 +489,6 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
end
def check_all_events
page.check('Active')
page.check('Push')
page.check('Issue')
page.check('Confidential issue')

View file

@ -3,8 +3,10 @@
require 'spec_helper'
describe 'Dashboard snippets' do
let_it_be(:user) { create(:user) }
context 'when the project has snippets' do
let(:project) { create(:project, :public) }
let(:project) { create(:project, :public, creator: user) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
before do
@ -22,7 +24,7 @@ describe 'Dashboard snippets' do
end
context 'when there are no project snippets', :js do
let(:project) { create(:project, :public) }
let(:project) { create(:project, :public, creator: user) }
before do
sign_in(project.owner)
@ -47,9 +49,49 @@ describe 'Dashboard snippets' do
end
end
context 'rendering file names' do
let_it_be(:snippet) { create(:personal_snippet, :public, author: user, file_name: 'foo.txt') }
let_it_be(:versioned_snippet) { create(:personal_snippet, :repository, :public, author: user, file_name: 'bar.txt') }
before do
sign_in(user)
end
context 'when feature flag :version_snippets is disabled' do
before do
stub_feature_flags(version_snippets: false)
visit dashboard_snippets_path
end
it 'contains the snippet file names from the DB' do
aggregate_failures do
expect(page).to have_content 'foo.txt'
expect(page).to have_content('bar.txt')
expect(page).not_to have_content('.gitattributes')
end
end
end
context 'when feature flag :version_snippets is enabled' do
before do
stub_feature_flags(version_snippets: true)
visit dashboard_snippets_path
end
it 'contains both the versioned and non-versioned filenames' do
aggregate_failures do
expect(page).to have_content 'foo.txt'
expect(page).to have_content('.gitattributes')
expect(page).not_to have_content('bar.txt')
end
end
end
end
context 'filtering by visibility' do
let(:user) { create(:user) }
let!(:snippets) do
let_it_be(:snippets) do
[
create(:personal_snippet, :public, author: user),
create(:personal_snippet, :internal, author: user),
@ -99,7 +141,7 @@ describe 'Dashboard snippets' do
end
context 'as an external user' do
let(:user) { create(:user, :external) }
let_it_be(:user) { create(:user, :external) }
before do
sign_in(user)

View file

@ -9,7 +9,7 @@ describe 'User activates issue tracker', :js do
let(:url) { 'http://tracker.example.com' }
def fill_short_form(disabled: false)
uncheck 'Active' if disabled
find('input[name="service[active]"] + button').click if disabled
fill_in 'service_project_url', with: url
fill_in 'service_issues_url', with: "#{url}/:id"

View file

@ -10,7 +10,7 @@ describe 'User activates Jira', :js do
let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
def fill_form(disabled: false)
uncheck 'Active' if disabled
find('input[name="service[active]"] + button').click if disabled
fill_in 'service_url', with: url
fill_in 'service_username', with: 'username'
@ -53,7 +53,6 @@ describe 'User activates Jira', :js do
it 'shows errors when some required fields are not filled in' do
click_link('Jira')
check 'Active'
fill_in 'service_password', with: 'password'
click_button('Test settings and save changes')

View file

@ -5,14 +5,13 @@ require 'spec_helper'
describe 'Set up Mattermost slash commands', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:service) { project.create_mattermost_slash_commands_service }
let(:mattermost_enabled) { true }
before do
stub_mattermost_setting(enabled: mattermost_enabled)
project.add_maintainer(user)
sign_in(user)
visit edit_project_service_path(project, service)
visit edit_project_service_path(project, :mattermost_slash_commands)
end
describe 'user visits the mattermost slash command config page' do
@ -30,6 +29,7 @@ describe 'Set up Mattermost slash commands', :js do
token = ('a'..'z').to_a.join
fill_in 'service_token', with: token
find('input[name="service[active]"] + button').click
click_on 'Save changes'
expect(current_path).to eq(project_settings_integrations_path(project))
@ -40,7 +40,6 @@ describe 'Set up Mattermost slash commands', :js do
token = ('a'..'z').to_a.join
fill_in 'service_token', with: token
check 'service_active'
click_on 'Save changes'
expect(current_path).to eq(project_settings_integrations_path(project))

View file

@ -5,12 +5,11 @@ require 'spec_helper'
describe 'Slack slash commands' do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:service) { project.create_slack_slash_commands_service }
before do
project.add_maintainer(user)
sign_in(user)
visit edit_project_service_path(project, service)
visit edit_project_service_path(project, :slack_slash_commands)
end
it 'shows a token placeholder' do
@ -23,17 +22,17 @@ describe 'Slack slash commands' do
expect(page).to have_content('This service allows users to perform common')
end
it 'redirects to the integrations page after saving but not activating' do
it 'redirects to the integrations page after saving but not activating', :js do
fill_in 'service_token', with: 'token'
find('input[name="service[active]"] + button').click
click_on 'Save'
expect(current_path).to eq(project_settings_integrations_path(project))
expect(page).to have_content('Slack slash commands settings saved, but not activated.')
end
it 'redirects to the integrations page after activating' do
it 'redirects to the integrations page after activating', :js do
fill_in 'service_token', with: 'token'
check 'service_active'
click_on 'Save'
expect(current_path).to eq(project_settings_integrations_path(project))

View file

@ -9,7 +9,7 @@ describe 'User activates issue tracker', :js do
let(:url) { 'http://tracker.example.com' }
def fill_form(disabled: false)
uncheck 'Active' if disabled
find('input[name="service[active]"] + button').click if disabled
fill_in 'service_project_url', with: url
fill_in 'service_issues_url', with: "#{url}/:id"

View file

@ -8,6 +8,7 @@ panel_groups:
type: "area-chart"
y_label: "y_label"
weight: 1
max_value: 1
metrics:
- id: metric_a1
query_range: 'query'

View file

@ -11,6 +11,7 @@
"type": { "type": "string" },
"y_label": { "type": "string" },
"y_axis": { "$ref": "axis.json" },
"max_value": { "type": "number" },
"weight": { "type": "number" },
"metrics": {
"type": "array",

View file

@ -0,0 +1,65 @@
import { mount } from '@vue/test-utils';
import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
import { GlToggle } from '@gitlab/ui';
const GL_TOGGLE_ACTIVE_CLASS = 'is-checked';
describe('ActiveToggle', () => {
let wrapper;
const defaultProps = {
initialActivated: true,
disabled: false,
};
const createComponent = props => {
wrapper = mount(ActiveToggle, {
propsData: Object.assign({}, defaultProps, props),
});
};
afterEach(() => {
if (wrapper) wrapper.destroy();
});
const findGlToggle = () => wrapper.find(GlToggle);
const findButtonInToggle = () => findGlToggle().find('button');
const findInputInToggle = () => findGlToggle().find('input');
describe('template', () => {
describe('initialActivated is false', () => {
it('renders GlToggle as inactive', () => {
createComponent({
initialActivated: false,
});
expect(findGlToggle().exists()).toBe(true);
expect(findButtonInToggle().classes()).not.toContain(GL_TOGGLE_ACTIVE_CLASS);
expect(findInputInToggle().attributes('value')).toBe('false');
});
});
describe('initialActivated is true', () => {
beforeEach(() => {
createComponent();
});
it('renders GlToggle as active', () => {
expect(findGlToggle().exists()).toBe(true);
expect(findButtonInToggle().classes()).toContain(GL_TOGGLE_ACTIVE_CLASS);
expect(findInputInToggle().attributes('value')).toBe('true');
});
describe('on toggle click', () => {
it('switches the form value', () => {
findButtonInToggle().trigger('click');
wrapper.vm.$nextTick(() => {
expect(findButtonInToggle().classes()).not.toContain(GL_TOGGLE_ACTIVE_CLASS);
expect(findInputInToggle().attributes('value')).toBe('false');
});
});
});
});
});
});

View file

@ -16,7 +16,9 @@ describe('NoteHeader component', () => {
const findActionsWrapper = () => wrapper.find({ ref: 'discussionActions' });
const findChevronIcon = () => wrapper.find({ ref: 'chevronIcon' });
const findActionText = () => wrapper.find({ ref: 'actionText' });
const findTimestampLink = () => wrapper.find({ ref: 'noteTimestampLink' });
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
const findSpinner = () => wrapper.find({ ref: 'spinner' });
const author = {
avatar_url: null,
@ -33,11 +35,7 @@ describe('NoteHeader component', () => {
store: new Vuex.Store({
actions,
}),
propsData: {
...props,
actionTextHtml: '',
noteId: '1394',
},
propsData: { ...props },
});
};
@ -108,17 +106,18 @@ describe('NoteHeader component', () => {
createComponent();
expect(findActionText().exists()).toBe(false);
expect(findTimestamp().exists()).toBe(false);
expect(findTimestampLink().exists()).toBe(false);
});
describe('when createdAt is passed as a prop', () => {
it('renders action text and a timestamp', () => {
createComponent({
createdAt: '2017-08-02T10:51:58.559Z',
noteId: 123,
});
expect(findActionText().exists()).toBe(true);
expect(findTimestamp().exists()).toBe(true);
expect(findTimestampLink().exists()).toBe(true);
});
it('renders correct actionText if passed', () => {
@ -133,8 +132,9 @@ describe('NoteHeader component', () => {
it('calls an action when timestamp is clicked', () => {
createComponent({
createdAt: '2017-08-02T10:51:58.559Z',
noteId: 123,
});
findTimestamp().trigger('click');
findTimestampLink().trigger('click');
expect(actions.setTargetNoteHash).toHaveBeenCalled();
});
@ -153,4 +153,30 @@ describe('NoteHeader component', () => {
expect(wrapper.find(GitlabTeamMemberBadge).exists()).toBe(expected);
},
);
describe('loading spinner', () => {
it('shows spinner when showSpinner is true', () => {
createComponent();
expect(findSpinner().exists()).toBe(true);
});
it('does not show spinner when showSpinner is false', () => {
createComponent({ showSpinner: false });
expect(findSpinner().exists()).toBe(false);
});
});
describe('timestamp', () => {
it('shows timestamp as a link if a noteId was provided', () => {
createComponent({ createdAt: new Date().toISOString(), noteId: 123 });
expect(findTimestampLink().exists()).toBe(true);
expect(findTimestamp().exists()).toBe(false);
});
it('shows timestamp as plain text if a noteId was not provided', () => {
createComponent({ createdAt: new Date().toISOString() });
expect(findTimestampLink().exists()).toBe(false);
expect(findTimestamp().exists()).toBe(true);
});
});
});

View file

@ -151,4 +151,35 @@ describe SnippetsHelper do
"<input type=\"text\" readonly=\"readonly\" class=\"js-snippet-url-area snippet-embed-input form-control\" data-url=\"#{url}\" value=\"<script src=&quot;#{url}.js&quot;></script>\" autocomplete=\"off\"></input>"
end
end
describe '#snippet_file_name' do
subject { helper.snippet_file_name(snippet) }
where(:snippet_type, :flag_enabled, :trait, :filename) do
[
[:personal_snippet, false, nil, 'foo.txt'],
[:personal_snippet, true, nil, 'foo.txt'],
[:personal_snippet, false, :repository, 'foo.txt'],
[:personal_snippet, true, :repository, '.gitattributes'],
[:project_snippet, false, nil, 'foo.txt'],
[:project_snippet, true, nil, 'foo.txt'],
[:project_snippet, false, :repository, 'foo.txt'],
[:project_snippet, true, :repository, '.gitattributes']
]
end
with_them do
let(:snippet) { create(snippet_type, trait, file_name: 'foo.txt') }
before do
allow(helper).to receive(:current_user).and_return(snippet.author)
stub_feature_flags(version_snippets: flag_enabled)
end
it 'returns the correct filename' do
expect(subject).to eq filename
end
end
end
end

View file

@ -23,9 +23,9 @@ describe('IntegrationSettingsForm', () => {
// Form Reference
expect(integrationSettingsForm.$form).toBeDefined();
expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM');
expect(integrationSettingsForm.formActive).toBeDefined();
// Form Child Elements
expect(integrationSettingsForm.$serviceToggle).toBeDefined();
expect(integrationSettingsForm.$submitBtn).toBeDefined();
expect(integrationSettingsForm.$submitBtnLoader).toBeDefined();
expect(integrationSettingsForm.$submitBtnLabel).toBeDefined();
@ -45,13 +45,15 @@ describe('IntegrationSettingsForm', () => {
});
it('should remove `novalidate` attribute to form when called with `true`', () => {
integrationSettingsForm.toggleServiceState(true);
integrationSettingsForm.formActive = true;
integrationSettingsForm.toggleServiceState();
expect(integrationSettingsForm.$form.attr('novalidate')).not.toBeDefined();
});
it('should set `novalidate` attribute to form when called with `false`', () => {
integrationSettingsForm.toggleServiceState(false);
integrationSettingsForm.formActive = false;
integrationSettingsForm.toggleServiceState();
expect(integrationSettingsForm.$form.attr('novalidate')).toBeDefined();
});
@ -66,8 +68,9 @@ describe('IntegrationSettingsForm', () => {
it('should set Save button label to "Test settings and save changes" when serviceActive & canTestService are `true`', () => {
integrationSettingsForm.canTestService = true;
integrationSettingsForm.formActive = true;
integrationSettingsForm.toggleSubmitBtnLabel(true);
integrationSettingsForm.toggleSubmitBtnLabel();
expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual(
'Test settings and save changes',
@ -76,18 +79,22 @@ describe('IntegrationSettingsForm', () => {
it('should set Save button label to "Save changes" when either serviceActive or canTestService (or both) is `false`', () => {
integrationSettingsForm.canTestService = false;
integrationSettingsForm.formActive = false;
integrationSettingsForm.toggleSubmitBtnLabel(false);
integrationSettingsForm.toggleSubmitBtnLabel();
expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
integrationSettingsForm.toggleSubmitBtnLabel(true);
integrationSettingsForm.formActive = true;
integrationSettingsForm.toggleSubmitBtnLabel();
expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
integrationSettingsForm.canTestService = true;
integrationSettingsForm.formActive = false;
integrationSettingsForm.toggleSubmitBtnLabel(false);
integrationSettingsForm.toggleSubmitBtnLabel();
expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
});

View file

@ -58,6 +58,7 @@ notes:
- system_note_metadata
- note_diff_file
- suggestions
- diff_note_positions
- review
label_links:
- target
@ -134,6 +135,7 @@ merge_requests:
- pipelines_for_merge_request
- merge_request_assignees
- suggestions
- diff_note_positions
- unresolved_notes
- assignees
- reviews
@ -517,6 +519,8 @@ error_tracking_setting:
- project
suggestions:
- note
diff_note_positions:
- note
metrics_setting:
- project
protected_environments:

View file

@ -25,7 +25,7 @@ describe Gitlab::ImportExport::Project::TreeRestorer, quarantine: { flaky: 'http
@project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
@shared = @project.import_export_shared
allow(Feature).to receive(:enabled?).and_call_original
allow(Feature).to receive(:enabled?) { true }
stub_feature_flags(project_import_ndjson: ndjson_enabled)
setup_import_export_config('complex')
@ -34,6 +34,7 @@ describe Gitlab::ImportExport::Project::TreeRestorer, quarantine: { flaky: 'http
allow_any_instance_of(Repository).to receive(:fetch_source_branch!).and_return(true)
allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false)
expect(@shared).not_to receive(:error)
expect_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch).with('feature', 'DCBA')
allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch)

View file

@ -29,12 +29,11 @@ describe Gitlab::ImportExport::Project::TreeSaver do
before_all do
RSpec::Mocks.with_temporary_scope do
allow(Feature).to receive(:enabled?).and_call_original
allow(Feature).to receive(:enabled?) { true }
stub_feature_flags(project_export_as_ndjson: ndjson_enabled)
project.add_maintainer(user)
stub_feature_flags(project_export_as_ndjson: ndjson_enabled)
project_tree_saver = described_class.new(project: project, current_user: user, shared: shared)
project_tree_saver.save

View file

@ -15,7 +15,8 @@ describe Gitlab::Metrics::Dashboard::Processor do
Gitlab::Metrics::Dashboard::Stages::CustomMetricsDetailsInserter,
Gitlab::Metrics::Dashboard::Stages::EndpointInserter,
Gitlab::Metrics::Dashboard::Stages::Sorter,
Gitlab::Metrics::Dashboard::Stages::AlertsInserter
Gitlab::Metrics::Dashboard::Stages::AlertsInserter,
Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter
]
end
@ -28,6 +29,12 @@ describe Gitlab::Metrics::Dashboard::Processor do
end
end
it 'includes an id for each dashboard panel' do
expect(all_panels).to satisfy_all do |panel|
panel[:id].present?
end
end
it 'includes boolean to indicate if panel group has custom metrics' do
expect(dashboard[:panel_groups]).to all(include( { has_custom_metrics: boolean } ))
end
@ -199,9 +206,11 @@ describe Gitlab::Metrics::Dashboard::Processor do
private
def all_metrics
dashboard[:panel_groups].flat_map do |group|
group[:panels].flat_map { |panel| panel[:metrics] }
end
all_panels.flat_map { |panel| panel[:metrics] }
end
def all_panels
dashboard[:panel_groups].flat_map { |group| group[:panels] }
end
def get_metric_details(metric)

View file

@ -63,5 +63,24 @@ describe Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter do
)
end
end
context 'when dashboard panels has unknown schema attributes' do
before do
error = ActiveModel::UnknownAttributeError.new(double, 'unknown_panel_attribute')
allow(::PerformanceMonitoring::PrometheusPanel).to receive(:new).and_raise(error)
end
it 'no panel has assigned id' do
transform!
expect(fetch_panel_ids(dashboard)).to all be_nil
end
it 'logs the failure' do
expect(Gitlab::ErrorTracking).to receive(:log_exception)
transform!
end
end
end
end

View file

@ -3,14 +3,35 @@
require 'spec_helper'
describe DiffNotePosition, type: :model do
it 'has a position attribute' do
diff_position = build(:diff_position)
line_code = 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521'
diff_note_position = build(:diff_note_position, line_code: line_code, position: diff_position)
describe '.create_or_update_by' do
context 'when a diff note' do
let(:note) { create(:diff_note_on_merge_request) }
let(:diff_position) { build(:diff_position) }
let(:line_code) { 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521' }
let(:diff_note_position) { note.diff_note_positions.first }
let(:params) { { diff_type: :head, line_code: line_code, position: diff_position } }
expect(diff_note_position.position).to eq(diff_position)
expect(diff_note_position.line_code).to eq(line_code)
expect(diff_note_position.diff_content_type).to eq('text')
context 'does not have a diff note position' do
it 'creates a diff note position' do
described_class.create_or_update_for(note, params)
expect(diff_note_position.position).to eq(diff_position)
expect(diff_note_position.line_code).to eq(line_code)
expect(diff_note_position.diff_content_type).to eq('text')
end
end
context 'has a diff note position' do
it 'updates the existing diff note position' do
create(:diff_note_position, note: note)
described_class.create_or_update_for(note, params)
expect(note.diff_note_positions.size).to eq(1)
expect(diff_note_position.position).to eq(diff_position)
expect(diff_note_position.line_code).to eq(line_code)
end
end
end
end
it 'unique by note_id and diff type' do

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'spec_helper'
describe Discussions::CaptureDiffNotePositionService do
context 'image note on diff' do
let!(:note) { create(:image_diff_note_on_merge_request) }
subject { described_class.new(note.noteable, ['files/images/any_image.png']) }
it 'is note affected by the service' do
expect(Gitlab::Diff::PositionTracer).not_to receive(:new)
expect(subject.execute(note.discussion)).to eq(nil)
expect(note.diff_note_positions).to be_empty
end
end
context 'when empty paths are passed as a param' do
let!(:note) { create(:diff_note_on_merge_request) }
subject { described_class.new(note.noteable, []) }
it 'does not calculate positons' do
expect(Gitlab::Diff::PositionTracer).not_to receive(:new)
expect(subject.execute(note.discussion)).to eq(nil)
expect(note.diff_note_positions).to be_empty
end
end
end

View file

@ -0,0 +1,67 @@
# frozen_string_literal: true
require 'spec_helper'
describe Discussions::CaptureDiffNotePositionsService do
context 'when merge request has a discussion' do
let(:source_branch) { 'compare-with-merge-head-source' }
let(:target_branch) { 'compare-with-merge-head-target' }
let(:merge_request) { create(:merge_request, source_branch: source_branch, target_branch: target_branch) }
let(:project) { merge_request.project }
let(:offset) { 30 }
let(:first_new_line) { 508 }
let(:second_new_line) { 521 }
let(:service) { described_class.new(merge_request) }
def build_position(new_line, diff_refs)
path = 'files/markdown/ruby-style-guide.md'
Gitlab::Diff::Position.new(old_path: path, new_path: path,
new_line: new_line, diff_refs: diff_refs)
end
def note_for(new_line)
position = build_position(new_line, merge_request.diff_refs)
create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request)
end
def verify_diff_note_position!(note, line)
id, old_line, new_line = note.line_code.split('_')
expect(new_line).to eq(line.to_s)
expect(note.diff_note_positions.size).to eq(1)
diff_position = note.diff_note_positions.last
diff_refs = Gitlab::Diff::DiffRefs.new(
base_sha: merge_request.target_branch_sha,
start_sha: merge_request.target_branch_sha,
head_sha: merge_request.merge_ref_head.sha)
expect(diff_position.line_code).to eq("#{id}_#{old_line.to_i - offset}_#{new_line}")
expect(diff_position.position).to eq(build_position(new_line.to_i, diff_refs))
end
let!(:first_discussion_note) { note_for(first_new_line) }
let!(:second_discussion_note) { note_for(second_new_line) }
let!(:second_discussion_another_note) do
create(:diff_note_on_merge_request,
project: project,
position: second_discussion_note.position,
discussion_id: second_discussion_note.discussion_id,
noteable: merge_request)
end
context 'and position of the discussion changed on target branch head' do
it 'diff positions are created for the first notes of the discussions' do
MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request)
service.execute
verify_diff_note_position!(first_discussion_note, first_new_line)
verify_diff_note_position!(second_discussion_note, second_new_line)
expect(second_discussion_another_note.diff_note_positions).to be_empty
end
end
end
end

View file

@ -33,6 +33,24 @@ describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shared_sta
expect(merge_request.merge_status).to eq('can_be_merged')
end
it 'update diff discussion positions' do
expect_next_instance_of(Discussions::CaptureDiffNotePositionsService) do |service|
expect(service).to receive(:execute)
end
subject
end
context 'when merge_ref_head_comments is disabled' do
it 'does not update diff discussion positions' do
stub_feature_flags(merge_ref_head_comments: false)
expect(Discussions::CaptureDiffNotePositionsService).not_to receive(:new)
subject
end
end
it 'updates the merge ref' do
expect { subject }.to change(merge_request, :merge_ref_head).from(nil)
end

View file

@ -143,10 +143,21 @@ describe Notes::CreateService do
end
it 'note is associated with a note diff file' do
MergeRequests::MergeToRefService.new(merge_request.project, merge_request.author).execute(merge_request)
note = described_class.new(project_with_repo, user, new_opts).execute
expect(note).to be_persisted
expect(note.note_diff_file).to be_present
expect(note.diff_note_positions).to be_present
end
it 'does not create diff positions merge_ref_head_comments is disabled' do
stub_feature_flags(merge_ref_head_comments: false)
expect(Discussions::CaptureDiffNotePositionService).not_to receive(:new)
described_class.new(project_with_repo, user, new_opts).execute
end
end
@ -160,6 +171,8 @@ describe Notes::CreateService do
end
it 'note is not associated with a note diff file' do
expect(Discussions::CaptureDiffNotePositionService).not_to receive(:new)
note = described_class.new(project_with_repo, user, new_opts).execute
expect(note).to be_persisted

View file

@ -73,7 +73,9 @@ module TestEnv
'submodule_inside_folder' => 'b491b92',
'png-lfs' => 'fe42f41',
'sha-starting-with-large-number' => '8426165',
'invalid-utf8-diff-paths' => '99e4853'
'invalid-utf8-diff-paths' => '99e4853',
'compare-with-merge-head-source' => 'b5f4399',
'compare-with-merge-head-target' => '2f1e176'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily

View file

@ -38,15 +38,12 @@ module ImportExport
end
def setup_reader(reader)
case reader
when :legacy_reader
allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(true)
allow_any_instance_of(Gitlab::ImportExport::JSON::NdjsonReader).to receive(:exist?).and_return(false)
when :ndjson_reader
if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson)
allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(false)
allow_any_instance_of(Gitlab::ImportExport::JSON::NdjsonReader).to receive(:exist?).and_return(true)
else
raise "invalid reader #{reader}. Supported readers: :legacy_reader, :ndjson_reader"
allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(true)
allow_any_instance_of(Gitlab::ImportExport::JSON::NdjsonReader).to receive(:exist?).and_return(false)
end
end