Commit b7c735c8 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 221b5297
<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>
import Vue from 'vue';
export default new Vue();
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,
},
});
},
});
};
...@@ -2,28 +2,33 @@ import $ from 'jquery'; ...@@ -2,28 +2,33 @@ import $ from 'jquery';
import axios from '../lib/utils/axios_utils'; import axios from '../lib/utils/axios_utils';
import flash from '../flash'; import flash from '../flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import initForm from './edit';
import eventHub from './edit/event_hub';
export default class IntegrationSettingsForm { export default class IntegrationSettingsForm {
constructor(formSelector) { constructor(formSelector) {
this.$form = $(formSelector); this.$form = $(formSelector);
this.formActive = false;
// Form Metadata // Form Metadata
this.canTestService = this.$form.data('canTest'); this.canTestService = this.$form.data('canTest');
this.testEndPoint = this.$form.data('testUrl'); this.testEndPoint = this.$form.data('testUrl');
// Form Child Elements // Form Child Elements
this.$serviceToggle = this.$form.find('#service_active');
this.$submitBtn = this.$form.find('button[type="submit"]'); this.$submitBtn = this.$form.find('button[type="submit"]');
this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner'); this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner');
this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label'); this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label');
} }
init() { init() {
// Initialize View // Init Vue component
this.toggleServiceState(this.$serviceToggle.is(':checked')); initForm(document.querySelector('.js-vue-integration-settings'));
eventHub.$on('toggle', active => {
this.formActive = active;
this.handleServiceToggle();
});
// Bind Event Listeners // Bind Event Listeners
this.$serviceToggle.on('change', e => this.handleServiceToggle(e));
this.$submitBtn.on('click', e => this.handleSettingsSave(e)); this.$submitBtn.on('click', e => this.handleSettingsSave(e));
} }
...@@ -31,7 +36,7 @@ export default class IntegrationSettingsForm { ...@@ -31,7 +36,7 @@ export default class IntegrationSettingsForm {
// Check if Service is marked active, as if not marked active, // Check if Service is marked active, as if not marked active,
// We can skip testing it and directly go ahead to allow form to // We can skip testing it and directly go ahead to allow form to
// be submitted // be submitted
if (!this.$serviceToggle.is(':checked')) { if (!this.formActive) {
return; return;
} }
...@@ -47,16 +52,16 @@ export default class IntegrationSettingsForm { ...@@ -47,16 +52,16 @@ export default class IntegrationSettingsForm {
} }
} }
handleServiceToggle(e) { handleServiceToggle() {
this.toggleServiceState($(e.currentTarget).is(':checked')); this.toggleServiceState();
} }
/** /**
* Change Form's validation enforcement based on service status (active/inactive) * Change Form's validation enforcement based on service status (active/inactive)
*/ */
toggleServiceState(serviceActive) { toggleServiceState() {
this.toggleSubmitBtnLabel(serviceActive); this.toggleSubmitBtnLabel();
if (serviceActive) { if (this.formActive) {
this.$form.removeAttr('novalidate'); this.$form.removeAttr('novalidate');
} else if (!this.$form.attr('novalidate')) { } else if (!this.$form.attr('novalidate')) {
this.$form.attr('novalidate', 'novalidate'); this.$form.attr('novalidate', 'novalidate');
...@@ -66,10 +71,10 @@ export default class IntegrationSettingsForm { ...@@ -66,10 +71,10 @@ export default class IntegrationSettingsForm {
/** /**
* Toggle Submit button label based on Integration status and ability to test service * Toggle Submit button label based on Integration status and ability to test service
*/ */
toggleSubmitBtnLabel(serviceActive) { toggleSubmitBtnLabel() {
let btnLabel = __('Save changes'); let btnLabel = __('Save changes');
if (serviceActive && this.canTestService) { if (this.formActive && this.canTestService) {
btnLabel = __('Test settings and save changes'); btnLabel = __('Test settings and save changes');
} }
......
...@@ -39,13 +39,18 @@ export default { ...@@ -39,13 +39,18 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
showSpinner: {
type: Boolean,
required: false,
default: true,
},
}, },
computed: { computed: {
toggleChevronClass() { toggleChevronClass() {
return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down'; return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down';
}, },
noteTimestampLink() { noteTimestampLink() {
return `#note_${this.noteId}`; return this.noteId ? `#note_${this.noteId}` : undefined;
}, },
hasAuthor() { hasAuthor() {
return this.author && Object.keys(this.author).length; return this.author && Object.keys(this.author).length;
...@@ -60,7 +65,9 @@ export default { ...@@ -60,7 +65,9 @@ export default {
this.$emit('toggleHandler'); this.$emit('toggleHandler');
}, },
updateTargetNoteHash() { updateTargetNoteHash() {
this.setTargetNoteHash(this.noteTimestampLink); if (this.$store) {
this.setTargetNoteHash(this.noteTimestampLink);
}
}, },
}, },
}; };
...@@ -101,16 +108,20 @@ export default { ...@@ -101,16 +108,20 @@ export default {
<template v-if="actionText">{{ actionText }}</template> <template v-if="actionText">{{ actionText }}</template>
</span> </span>
<a <a
ref="noteTimestamp" v-if="noteTimestampLink"
ref="noteTimestampLink"
:href="noteTimestampLink" :href="noteTimestampLink"
class="note-timestamp system-note-separator" class="note-timestamp system-note-separator"
@click="updateTargetNoteHash" @click="updateTargetNoteHash"
> >
<time-ago-tooltip :time="createdAt" tooltip-placement="bottom" /> <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" />
</a> </a>
<time-ago-tooltip v-else ref="noteTimestamp" :time="createdAt" tooltip-placement="bottom" />
</template> </template>
<slot name="extra-controls"></slot> <slot name="extra-controls"></slot>
<i <i
v-if="showSpinner"
ref="spinner"
class="fa fa-spinner fa-spin editing-spinner" class="fa fa-spinner fa-spin editing-spinner"
:aria-label="__('Comment is being updated')" :aria-label="__('Comment is being updated')"
aria-hidden="true" aria-hidden="true"
......
...@@ -9,7 +9,7 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -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_issue_summary, project, default_enabled: true)
push_frontend_feature_flag(:release_evidence_collection, 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_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 end
before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_update_release!, only: %i[edit update]
......
...@@ -160,4 +160,14 @@ module SnippetsHelper ...@@ -160,4 +160,14 @@ module SnippetsHelper
title: 'Download', title: 'Download',
rel: 'noopener noreferrer') rel: 'noopener noreferrer')
end 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 end
...@@ -28,9 +28,20 @@ class DiffNotePosition < ApplicationRecord ...@@ -28,9 +28,20 @@ class DiffNotePosition < ApplicationRecord
end end
def position=(position) 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 = position.to_h
position_attrs[:diff_content_type] = position_attrs.delete(:position_type) position_attrs[:diff_content_type] = position_attrs.delete(:position_type)
position_attrs
assign_attributes(position_attrs)
end end
end end
...@@ -83,6 +83,7 @@ class Note < ApplicationRecord ...@@ -83,6 +83,7 @@ class Note < ApplicationRecord
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata has_one :system_note_metadata
has_one :note_diff_file, inverse_of: :diff_note, foreign_key: :diff_note_id 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 :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true delegate :name, to: :project, prefix: true
......
...@@ -4,7 +4,7 @@ module PerformanceMonitoring ...@@ -4,7 +4,7 @@ module PerformanceMonitoring
class PrometheusPanel class PrometheusPanel
include ActiveModel::Model 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 :title, presence: true
validates :metrics, presence: true validates :metrics, presence: true
......
# 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
# 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
...@@ -118,11 +118,18 @@ module MergeRequests ...@@ -118,11 +118,18 @@ module MergeRequests
if can_git_merge? && merge_to_ref if can_git_merge? && merge_to_ref
merge_request.mark_as_mergeable merge_request.mark_as_mergeable
update_diff_discussion_positions!
else else
merge_request.mark_as_unmergeable merge_request.mark_as_unmergeable
end end
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! def recheck!
if !merge_request.recheck_merge_status? && outdated_merge_ref? if !merge_request.recheck_merge_status? && outdated_merge_ref?
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
......
...@@ -65,6 +65,10 @@ module Notes ...@@ -65,6 +65,10 @@ module Notes
if Feature.enabled?(:notes_create_service_tracking, project) if Feature.enabled?(:notes_create_service_tracking, project)
Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note)) Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note))
end 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 end
def do_commands(note, update_params, message, only_commands) def do_commands(note, update_params, message, only_commands)
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
= render 'groups/settings/project_creation_level', f: f, group: @group = render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/subgroup_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 '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 = 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' } = f.submit _('Save changes'), class: 'btn btn-success prepend-top-default js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' }
...@@ -8,11 +8,7 @@ ...@@ -8,11 +8,7 @@
= markdown @service.help = markdown @service.help
.service-settings .service-settings
- if @service.show_active_box? .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 } }
.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)
- if @service.configurable_events.present? - if @service.configurable_events.present?
.form-group.row .form-group.row
......
- link_project = local_assigns.fetch(:link_project, false) - link_project = local_assigns.fetch(:link_project, false)
- notes_count = @noteable_meta_data[snippet.id].user_notes_count - notes_count = @noteable_meta_data[snippet.id].user_notes_count
- file_name = snippet_file_name(snippet)
%li.snippet-row.py-3 %li.snippet-row.py-3
= image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: '' = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
...@@ -7,10 +8,10 @@ ...@@ -7,10 +8,10 @@
.title .title
= link_to gitlab_snippet_path(snippet) do = link_to gitlab_snippet_path(snippet) do
= snippet.title = snippet.title
- if snippet.file_name.present? - if file_name.present?
%span.snippet-filename.d-none.d-sm-inline-block.ml-2 %span.snippet-filename.d-none.d-sm-inline-block.ml-2
= sprite_icon('doc-code', size: 16, css_class: 'file-icon align-text-bottom') = sprite_icon('doc-code', size: 16, css_class: 'file-icon align-text-bottom')
= snippet.file_name = file_name
%ul.controls %ul.controls
%li %li
......
...@@ -944,7 +944,7 @@ ...@@ -944,7 +944,7 @@
- :name: background_migration - :name: background_migration
:feature_category: :database :feature_category: :database
:has_external_dependencies: :has_external_dependencies:
:urgency: :low :urgency: :throttled
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
:idempotent: :idempotent:
......
...@@ -4,6 +4,7 @@ class BackgroundMigrationWorker # rubocop:disable Scalability/IdempotentWorker ...@@ -4,6 +4,7 @@ class BackgroundMigrationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker include ApplicationWorker
feature_category :database feature_category :database
urgency :throttled
# The minimum amount of time between processing two jobs of the same migration # The minimum amount of time between processing two jobs of the same migration
# class. # class.
......
---
title: Update Active checkbox component to use toggle
merge_request: 27778
author:
type: added
---
title: Deprecate 'token' attribute from Runners API
merge_request: 29481
author:
type: deprecated
---
title: Fix dashboard processing error which prevented dashboards with unknown attributes
inside panels from being displayed
merge_request: 29517
author:
type: fixed
---
title: Migrate legacy uploads out of deprecated paths
merge_request: 29295
author:
type: fixed
---
title: Allow Release links to be edited on the Edit Release page
merge_request: 28816
author:
type: added
# 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
...@@ -13152,5 +13152,6 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -13152,5 +13152,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200408110856 20200408110856
20200408153842 20200408153842
20200408175424 20200408175424
20200409211607
\. \.
...@@ -111,24 +111,6 @@ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, MergeReque ...@@ -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]" 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 ## Migrate from object storage to local storage
If you need to disable Object Storage for any reason, first you need to migrate If you need to disable Object Storage for any reason, first you need to migrate
......
...@@ -162,6 +162,10 @@ GET /runners/:id ...@@ -162,6 +162,10 @@ GET /runners/:id
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6" 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: Example response:
```json ```json
...@@ -221,6 +225,10 @@ PUT /runners/:id ...@@ -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" 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: Example response:
```json ```json
......
...@@ -112,6 +112,32 @@ To access the Credentials inventory of a group, navigate to **{shield}** **Secur ...@@ -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). 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 ##### Outer forks restriction for Group-managed accounts
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34648) in GitLab 12.9. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34648) in GitLab 12.9.
......
...@@ -39,7 +39,7 @@ service in GitLab. ...@@ -39,7 +39,7 @@ service in GitLab.
1. Navigate to the project you want to configure to trigger builds. 1. Navigate to the project you want to configure to trigger builds.
1. Navigate to the [Integrations page](overview.md#accessing-integrations) 1. Navigate to the [Integrations page](overview.md#accessing-integrations)
1. Click 'Atlassian Bamboo CI' 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 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 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 up from the Project Key and Plan Key that are set on project/plan creation and
......
...@@ -21,7 +21,7 @@ With the webhook URL created in the Discord channel, you can set up the Discord ...@@ -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. 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. 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. 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. Paste the webhook URL that you copied from the create Discord webhook step.
1. Configure the remaining options and click the **Save changes** button. 1. Configure the remaining options and click the **Save changes** button.
......
...@@ -18,7 +18,7 @@ To set up the generic alerts integration: ...@@ -18,7 +18,7 @@ To set up the generic alerts integration:
1. Navigate to **Settings > Integrations** in a project. 1. Navigate to **Settings > Integrations** in a project.
1. Click on **Alerts endpoint**. 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 ## Customizing the payload
......
...@@ -27,7 +27,7 @@ with `repo:status` access granted: ...@@ -27,7 +27,7 @@ with `repo:status` access granted:
1. Navigate to the project you want to configure. 1. Navigate to the project you want to configure.
1. Navigate to the [Integrations page](overview.md#accessing-integrations) 1. Navigate to the [Integrations page](overview.md#accessing-integrations)
1. Click "GitHub". 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. 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. 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. 1. Optionally uncheck **Static status check names** checkbox to disable static status check names.
......
...@@ -19,7 +19,7 @@ When you have the **Webhook URL** for your Hangouts Chat room webhook, you can s ...@@ -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. 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. 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. 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. Paste the **Webhook URL** that you copied from the Hangouts Chat configuration step.
1. Configure the remaining options and click `Save changes`. 1. Configure the remaining options and click `Save changes`.
......
...@@ -37,7 +37,7 @@ service in GitLab. ...@@ -37,7 +37,7 @@ service in GitLab.
1. Navigate to the project you want to configure for notifications. 1. Navigate to the project you want to configure for notifications.
1. Navigate to the [Integrations page](overview.md#accessing-integrations) 1. Navigate to the [Integrations page](overview.md#accessing-integrations)
1. Click "HipChat". 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 `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. Insert the `room` field from the URL into the `Room` field on the Web page.
1. Save or optionally click "Test Settings". 1. Save or optionally click "Test Settings".
......
...@@ -28,7 +28,7 @@ need to follow the firsts steps of the next section. ...@@ -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 project you want to configure for notifications.
1. Navigate to the [Integrations page](overview.md#accessing-integrations) 1. Navigate to the [Integrations page](overview.md#accessing-integrations)
1. Click "Irker". 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`) 1. Enter the server host address where `irkerd` runs (defaults to `localhost`)
in the `Server host` field on the Web page in the `Server host` field on the Web page
1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the 1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the
......
...@@ -103,7 +103,7 @@ in a new slash command. ...@@ -103,7 +103,7 @@ in a new slash command.
### Step 4. Copy the Mattermost token into the Mattermost slash command service ### 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 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) ![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png)
......
...@@ -14,7 +14,7 @@ The Slack Notifications Service allows your GitLab project to send events (e.g. ...@@ -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. 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. 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. 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. 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. 1. Paste the **Webhook URL** that you copied from the Slack Configuration step.
......
...@@ -19,7 +19,7 @@ For GitLab.com, use the [Slack app](gitlab_slack_application.md) instead. ...@@ -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. 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. 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. 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) ![Slack setup instructions](img/slack_setup.png)
......
...@@ -17,7 +17,7 @@ When you have the **Webhook URL** for your Unify Circuit conversation webhook, y ...@@ -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. 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. 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. 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. Paste the **Webhook URL** that you copied from the Unify Circuit configuration step.
1. Configure the remaining options and click `Save changes`. 1. Configure the remaining options and click `Save changes`.
......
...@@ -309,12 +309,12 @@ Here is an example of a Release Evidence object: ...@@ -309,12 +309,12 @@ Here is an example of a Release Evidence object:
### Enabling Release Evidence display **(CORE ONLY)** ### Enabling Release Evidence display **(CORE ONLY)**
This feature comes with the `:release_evidence_collection` feature flag 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 ask a GitLab administrator with Rails console access to run the following
command: command:
```ruby ```ruby
Feature.enable(:release_evidence_collection) Feature.disable(:release_evidence_collection)
``` ```
NOTE: **Note:** NOTE: **Note:**
......
...@@ -10,7 +10,11 @@ module API ...@@ -10,7 +10,11 @@ module API
expose :access_level expose :access_level
expose :version, :revision, :platform, :architecture expose :version, :revision, :platform, :architecture
expose :contacted_at 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? } expose :token, if: lambda { |runner, options| options[:current_user].admin? || !runner.instance_type? }
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
expose :projects, with: Entities::BasicProjectDetails do |runner, options| expose :projects, with: Entities::BasicProjectDetails do |runner, options|
if options[:current_user].admin? if options[:current_user].admin?
......
...@@ -70,8 +70,7 @@ module Gitlab ...@@ -70,8 +70,7 @@ module Gitlab
# Do not create relation if it is: # Do not create relation if it is:
# - An unknown service # - An unknown service
# - A legacy trigger # - A legacy trigger
unknown_service? || unknown_service? || legacy_trigger?
(!Feature.enabled?(:use_legacy_pipeline_triggers, @importable) && legacy_trigger?)
end end
def setup_models def setup_models
......
...@@ -15,6 +15,9 @@ module Gitlab ...@@ -15,6 +15,9 @@ module Gitlab
insert_panel_id(id, panel) insert_panel_id(id, panel)
end end
rescue ActiveModel::UnknownAttributeError => error
remove_panel_ids!
Gitlab::ErrorTracking.log_exception(error)
end end
private private
......
# 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
...@@ -385,6 +385,11 @@ msgstr "" ...@@ -385,6 +385,11 @@ msgstr ""
msgid "%{name}'s avatar" msgid "%{name}'s avatar"
msgstr "" 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" msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "" msgstr ""
...@@ -10840,6 +10845,9 @@ 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." 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 "" 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" msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr "" msgstr ""
...@@ -23015,6 +23023,15 @@ msgstr "" ...@@ -23015,6 +23023,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}" msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr "" 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." msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr "" msgstr ""
...@@ -25030,6 +25047,9 @@ msgstr "" ...@@ -25030,6 +25047,9 @@ msgstr ""
msgid "no contributions" msgid "no contributions"
msgstr "" msgstr ""
msgid "no expiration"
msgstr ""
msgid "no one can merge" msgid "no one can merge"
msgstr "" msgstr ""
...@@ -25315,6 +25335,9 @@ msgstr "" ...@@ -25315,6 +25335,9 @@ msgstr ""
msgid "view the blob" msgid "view the blob"
msgstr "" msgstr ""
msgid "vulnerability|Add a comment"
msgstr ""
msgid "vulnerability|Add a comment or reason for dismissal" msgid "vulnerability|Add a comment or reason for dismissal"
msgstr "" msgstr ""
......
...@@ -489,7 +489,6 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc ...@@ -489,7 +489,6 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
end end
def check_all_events def check_all_events
page.check('Active')
page.check('Push') page.check('Push')
page.check('Issue') page.check('Issue')
page.check('Confidential issue') page.check('Confidential issue')
......
...@@ -3,8 +3,10 @@ ...@@ -3,8 +3,10 @@
require 'spec_helper' require 'spec_helper'
describe 'Dashboard snippets' do describe 'Dashboard snippets' do
let_it_be(:user) { create(:user) }
context 'when the project has snippets' do 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) } let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
before do before do
...@@ -22,7 +24,7 @@ describe 'Dashboard snippets' do ...@@ -22,7 +24,7 @@ describe 'Dashboard snippets' do
end end
context 'when there are no project snippets', :js do context 'when there are no project snippets', :js do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public, creator: user) }
before do before do
sign_in(project.owner) sign_in(project.owner)
...@@ -47,9 +49,49 @@ describe 'Dashboard snippets' do ...@@ -47,9 +49,49 @@ describe 'Dashboard snippets' do
end end
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 context 'filtering by visibility' do
let(:user) { create(:user) } let_it_be(:snippets) do
let!(:snippets) do
[ [
create(:personal_snippet, :public, author: user), create(:personal_snippet, :public, author: user),
create(:personal_snippet, :internal, author: user), create(:personal_snippet, :internal, author: user),
...@@ -99,7 +141,7 @@ describe 'Dashboard snippets' do ...@@ -99,7 +141,7 @@ describe 'Dashboard snippets' do
end end
context 'as an external user' do context 'as an external user' do
let(:user) { create(:user, :external) } let_it_be(:user) { create(:user, :external) }
before do before do
sign_in(user) sign_in(user)
......
...@@ -9,7 +9,7 @@ describe 'User activates issue tracker', :js do ...@@ -9,7 +9,7 @@ describe 'User activates issue tracker', :js do
let(:url) { 'http://tracker.example.com' } let(:url) { 'http://tracker.example.com' }
def fill_short_form(disabled: false) 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_project_url', with: url
fill_in 'service_issues_url', with: "#{url}/:id" fill_in 'service_issues_url', with: "#{url}/:id"
......
...@@ -10,7 +10,7 @@ describe 'User activates Jira', :js do ...@@ -10,7 +10,7 @@ describe 'User activates Jira', :js do
let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' } let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
def fill_form(disabled: false) 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_url', with: url
fill_in 'service_username', with: 'username' fill_in 'service_username', with: 'username'
...@@ -53,7 +53,6 @@ describe 'User activates Jira', :js do ...@@ -53,7 +53,6 @@ describe 'User activates Jira', :js do
it 'shows errors when some required fields are not filled in' do it 'shows errors when some required fields are not filled in' do
click_link('Jira') click_link('Jira')
check 'Active'
fill_in 'service_password', with: 'password' fill_in 'service_password', with: 'password'
click_button('Test settings and save changes') click_button('Test settings and save changes')
......
...@@ -5,14 +5,13 @@ require 'spec_helper' ...@@ -5,14 +5,13 @@ require 'spec_helper'
describe 'Set up Mattermost slash commands', :js do describe 'Set up Mattermost slash commands', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:service) { project.create_mattermost_slash_commands_service }
let(:mattermost_enabled) { true } let(:mattermost_enabled) { true }
before do before do
stub_mattermost_setting(enabled: mattermost_enabled) stub_mattermost_setting(enabled: mattermost_enabled)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
visit edit_project_service_path(project, service) visit edit_project_service_path(project, :mattermost_slash_commands)
end end
describe 'user visits the mattermost slash command config page' do describe 'user visits the mattermost slash command config page' do
...@@ -30,6 +29,7 @@ describe 'Set up Mattermost slash commands', :js do ...@@ -30,6 +29,7 @@ describe 'Set up Mattermost slash commands', :js do
token = ('a'..'z').to_a.join token = ('a'..'z').to_a.join
fill_in 'service_token', with: token fill_in 'service_token', with: token
find('input[name="service[active]"] + button').click
click_on 'Save changes' click_on 'Save changes'
expect(current_path).to eq(project_settings_integrations_path(project)) expect(current_path).to eq(project_settings_integrations_path(project))
...@@ -40,7 +40,6 @@ describe 'Set up Mattermost slash commands', :js do ...@@ -40,7 +40,6 @@ describe 'Set up Mattermost slash commands', :js do
token = ('a'..'z').to_a.join token = ('a'..'z').to_a.join
fill_in 'service_token', with: token fill_in 'service_token', with: token
check 'service_active'
click_on 'Save changes' click_on 'Save changes'
expect(current_path).to eq(project_settings_integrations_path(project)) expect(current_path).to eq(project_settings_integrations_path(project))
......
...@@ -5,12 +5,11 @@ require 'spec_helper' ...@@ -5,12 +5,11 @@ require 'spec_helper'
describe 'Slack slash commands' do describe 'Slack slash commands' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:service) { project.create_slack_slash_commands_service }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
visit edit_project_service_path(project, service) visit edit_project_service_path(project, :slack_slash_commands)
end end
it 'shows a token placeholder' do it 'shows a token placeholder' do
...@@ -23,17 +22,17 @@ describe 'Slack slash commands' do ...@@ -23,17 +22,17 @@ describe 'Slack slash commands' do
expect(page).to have_content('This service allows users to perform common') expect(page).to have_content('This service allows users to perform common')
end 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' fill_in 'service_token', with: 'token'
find('input[name="service[active]"] + button').click
click_on 'Save' click_on 'Save'
expect(current_path).to eq(project_settings_integrations_path(project)) expect(current_path).to eq(project_settings_integrations_path(project))
expect(page).to have_content('Slack slash commands settings saved, but not activated.') expect(page).to have_content('Slack slash commands settings saved, but not activated.')
end 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' fill_in 'service_token', with: 'token'
check 'service_active'
click_on 'Save' click_on 'Save'
expect(current_path).to eq(project_settings_integrations_path(project)) expect(current_path).to eq(project_settings_integrations_path(project))
......
...@@ -9,7 +9,7 @@ describe 'User activates issue tracker', :js do ...@@ -9,7 +9,7 @@ describe 'User activates issue tracker', :js do
let(:url) { 'http://tracker.example.com' } let(:url) { 'http://tracker.example.com' }
def fill_form(disabled: false) 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_project_url', with: url
fill_in 'service_issues_url', with: "#{url}/:id" fill_in 'service_issues_url', with: "#{url}/:id"
......
...@@ -8,6 +8,7 @@ panel_groups: ...@@ -8,6 +8,7 @@ panel_groups:
type: "area-chart" type: "area-chart"
y_label: "y_label" y_label: "y_label"
weight: 1 weight: 1
max_value: 1
metrics: metrics:
- id: metric_a1 - id: metric_a1
query_range: 'query' query_range: 'query'
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"type": { "type": "string" }, "type": { "type": "string" },
"y_label": { "type": "string" }, "y_label": { "type": "string" },
"y_axis": { "$ref": "axis.json" }, "y_axis": { "$ref": "axis.json" },
"max_value": { "type": "number" },
"weight": { "type": "number" }, "weight": { "type": "number" },
"metrics": { "metrics": {
"type": "array", "type": "array",
......
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');
});
});
});
});
});
});
...@@ -16,7 +16,9 @@ describe('NoteHeader component', () => { ...@@ -16,7 +16,9 @@ describe('NoteHeader component', () => {
const findActionsWrapper = () => wrapper.find({ ref: 'discussionActions' }); const findActionsWrapper = () => wrapper.find({ ref: 'discussionActions' });
const findChevronIcon = () => wrapper.find({ ref: 'chevronIcon' }); const findChevronIcon = () => wrapper.find({ ref: 'chevronIcon' });
const findActionText = () => wrapper.find({ ref: 'actionText' }); const findActionText = () => wrapper.find({ ref: 'actionText' });
const findTimestampLink = () => wrapper.find({ ref: 'noteTimestampLink' });
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' }); const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
const findSpinner = () => wrapper.find({ ref: 'spinner' });
const author = { const author = {
avatar_url: null, avatar_url: null,
...@@ -33,11 +35,7 @@ describe('NoteHeader component', () => { ...@@ -33,11 +35,7 @@ describe('NoteHeader component', () => {
store: new Vuex.Store({ store: new Vuex.Store({
actions, actions,
}), }),
propsData: { propsData: { ...props },
...props,
actionTextHtml: '',
noteId: '1394',
},
}); });
}; };
...@@ -108,17 +106,18 @@ describe('NoteHeader component', () => { ...@@ -108,17 +106,18 @@ describe('NoteHeader component', () => {
createComponent(); createComponent();
expect(findActionText().exists()).toBe(false); expect(findActionText().exists()).toBe(false);
expect(findTimestamp().exists()).toBe(false); expect(findTimestampLink().exists()).toBe(false);
}); });
describe('when createdAt is passed as a prop', () => { describe('when createdAt is passed as a prop', () => {
it('renders action text and a timestamp', () => { it('renders action text and a timestamp', () => {
createComponent({ createComponent({
createdAt: '2017-08-02T10:51:58.559Z', createdAt: '2017-08-02T10:51:58.559Z',
noteId: 123,
}); });
expect(findActionText().exists()).toBe(true); expect(findActionText().exists()).toBe(true);
expect(findTimestamp().exists()).toBe(true); expect(findTimestampLink().exists()).toBe(true);
}); });
it('renders correct actionText if passed', () => { it('renders correct actionText if passed', () => {
...@@ -133,8 +132,9 @@ describe('NoteHeader component', () => { ...@@ -133,8 +132,9 @@ describe('NoteHeader component', () => {
it('calls an action when timestamp is clicked', () => { it('calls an action when timestamp is clicked', () => {
createComponent({ createComponent({
createdAt: '2017-08-02T10:51:58.559Z', createdAt: '2017-08-02T10:51:58.559Z',
noteId: 123,
}); });
findTimestamp().trigger('click'); findTimestampLink().trigger('click');
expect(actions.setTargetNoteHash).toHaveBeenCalled(); expect(actions.setTargetNoteHash).toHaveBeenCalled();
}); });
...@@ -153,4 +153,30 @@ describe('NoteHeader component', () => { ...@@ -153,4 +153,30 @@ describe('NoteHeader component', () => {
expect(wrapper.find(GitlabTeamMemberBadge).exists()).toBe(expected); 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);
});
});
}); });
...@@ -151,4 +151,35 @@ describe SnippetsHelper do ...@@ -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>" "<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
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 end
...@@ -23,9 +23,9 @@ describe('IntegrationSettingsForm', () => { ...@@ -23,9 +23,9 @@ describe('IntegrationSettingsForm', () => {
// Form Reference // Form Reference
expect(integrationSettingsForm.$form).toBeDefined(); expect(integrationSettingsForm.$form).toBeDefined();
expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM'); expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM');
expect(integrationSettingsForm.formActive).toBeDefined();
// Form Child Elements // Form Child Elements
expect(integrationSettingsForm.$serviceToggle).toBeDefined();
expect(integrationSettingsForm.$submitBtn).toBeDefined(); expect(integrationSettingsForm.$submitBtn).toBeDefined();
expect(integrationSettingsForm.$submitBtnLoader).toBeDefined(); expect(integrationSettingsForm.$submitBtnLoader).toBeDefined();
expect(integrationSettingsForm.$submitBtnLabel).toBeDefined(); expect(integrationSettingsForm.$submitBtnLabel).toBeDefined();
...@@ -45,13 +45,15 @@ describe('IntegrationSettingsForm', () => { ...@@ -45,13 +45,15 @@ describe('IntegrationSettingsForm', () => {
}); });
it('should remove `novalidate` attribute to form when called with `true`', () => { 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(); expect(integrationSettingsForm.$form.attr('novalidate')).not.toBeDefined();
}); });
it('should set `novalidate` attribute to form when called with `false`', () => { 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(); expect(integrationSettingsForm.$form.attr('novalidate')).toBeDefined();
}); });
...@@ -66,8 +68,9 @@ describe('IntegrationSettingsForm', () => { ...@@ -66,8 +68,9 @@ describe('IntegrationSettingsForm', () => {
it('should set Save button label to "Test settings and save changes" when serviceActive & canTestService are `true`', () => { it('should set Save button label to "Test settings and save changes" when serviceActive & canTestService are `true`', () => {
integrationSettingsForm.canTestService = true; integrationSettingsForm.canTestService = true;
integrationSettingsForm.formActive = true;
integrationSettingsForm.toggleSubmitBtnLabel(true); integrationSettingsForm.toggleSubmitBtnLabel();
expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual( expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual(
'Test settings and save changes', 'Test settings and save changes',
...@@ -76,18 +79,22 @@ describe('IntegrationSettingsForm', () => { ...@@ -76,18 +79,22 @@ describe('IntegrationSettingsForm', () => {
it('should set Save button label to "Save changes" when either serviceActive or canTestService (or both) is `false`', () => { it('should set Save button label to "Save changes" when either serviceActive or canTestService (or both) is `false`', () => {
integrationSettingsForm.canTestService = false; integrationSettingsForm.canTestService = false;
integrationSettingsForm.formActive = false;
integrationSettingsForm.toggleSubmitBtnLabel(false); integrationSettingsForm.toggleSubmitBtnLabel();
expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes'); expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
integrationSettingsForm.toggleSubmitBtnLabel(true); integrationSettingsForm.formActive = true;
integrationSettingsForm.toggleSubmitBtnLabel();
expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes'); expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
integrationSettingsForm.canTestService = true; integrationSettingsForm.canTestService = true;
integrationSettingsForm.formActive = false;
integrationSettingsForm.toggleSubmitBtnLabel(false); integrationSettingsForm.toggleSubmitBtnLabel();
expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes'); expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
}); });
......
...@@ -58,6 +58,7 @@ notes: ...@@ -58,6 +58,7 @@ notes:
- system_note_metadata - system_note_metadata
- note_diff_file - note_diff_file
- suggestions - suggestions
- diff_note_positions
- review - review
label_links: label_links:
- target - target
...@@ -134,6 +135,7 @@ merge_requests: ...@@ -134,6 +135,7 @@ merge_requests:
- pipelines_for_merge_request - pipelines_for_merge_request
- merge_request_assignees - merge_request_assignees
- suggestions - suggestions
- diff_note_positions
- unresolved_notes - unresolved_notes
- assignees - assignees
- reviews - reviews
...@@ -517,6 +519,8 @@ error_tracking_setting: ...@@ -517,6 +519,8 @@ error_tracking_setting:
- project - project
suggestions: suggestions:
- note - note
diff_note_positions:
- note
metrics_setting: metrics_setting:
- project - project
protected_environments: protected_environments:
......
...@@ -25,7 +25,7 @@ describe Gitlab::ImportExport::Project::TreeRestorer, quarantine: { flaky: 'http ...@@ -25,7 +25,7 @@ describe Gitlab::ImportExport::Project::TreeRestorer, quarantine: { flaky: 'http
@project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') @project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
@shared = @project.import_export_shared @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) stub_feature_flags(project_import_ndjson: ndjson_enabled)
setup_import_export_config('complex') setup_import_export_config('complex')
...@@ -34,6 +34,7 @@ describe Gitlab::ImportExport::Project::TreeRestorer, quarantine: { flaky: 'http ...@@ -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(Repository).to receive(:fetch_source_branch!).and_return(true)
allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false) 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') 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) allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch)
......
...@@ -29,12 +29,11 @@ describe Gitlab::ImportExport::Project::TreeSaver do ...@@ -29,12 +29,11 @@ describe Gitlab::ImportExport::Project::TreeSaver do
before_all do before_all do
RSpec::Mocks.with_temporary_scope 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) stub_feature_flags(project_export_as_ndjson: ndjson_enabled)
project.add_maintainer(user) 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 = described_class.new(project: project, current_user: user, shared: shared)
project_tree_saver.save project_tree_saver.save
......
...@@ -15,7 +15,8 @@ describe Gitlab::Metrics::Dashboard::Processor do ...@@ -15,7 +15,8 @@ describe Gitlab::Metrics::Dashboard::Processor do
Gitlab::Metrics::Dashboard::Stages::CustomMetricsDetailsInserter, Gitlab::Metrics::Dashboard::Stages::CustomMetricsDetailsInserter,
Gitlab::Metrics::Dashboard::Stages::EndpointInserter, Gitlab::Metrics::Dashboard::Stages::EndpointInserter,
Gitlab::Metrics::Dashboard::Stages::Sorter, Gitlab::Metrics::Dashboard::Stages::Sorter,
Gitlab::Metrics::Dashboard::Stages::AlertsInserter Gitlab::Metrics::Dashboard::Stages::AlertsInserter,
Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter
] ]
end end
...@@ -28,6 +29,12 @@ describe Gitlab::Metrics::Dashboard::Processor do ...@@ -28,6 +29,12 @@ describe Gitlab::Metrics::Dashboard::Processor do
end end
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 it 'includes boolean to indicate if panel group has custom metrics' do
expect(dashboard[:panel_groups]).to all(include( { has_custom_metrics: boolean } )) expect(dashboard[:panel_groups]).to all(include( { has_custom_metrics: boolean } ))
end end
...@@ -199,9 +206,11 @@ describe Gitlab::Metrics::Dashboard::Processor do ...@@ -199,9 +206,11 @@ describe Gitlab::Metrics::Dashboard::Processor do
private private
def all_metrics def all_metrics
dashboard[:panel_groups].flat_map do |group| all_panels.flat_map { |panel| panel[:metrics] }
group[:panels].flat_map { |panel| panel[:metrics] } end
end
def all_panels
dashboard[:panel_groups].flat_map { |group| group[:panels] }
end end
def get_metric_details(metric) def get_metric_details(metric)
......
...@@ -63,5 +63,24 @@ describe Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter do ...@@ -63,5 +63,24 @@ describe Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter do
) )
end end
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
end end
...@@ -3,14 +3,35 @@ ...@@ -3,14 +3,35 @@
require 'spec_helper' require 'spec_helper'
describe DiffNotePosition, type: :model do describe DiffNotePosition, type: :model do
it 'has a position attribute' do describe '.create_or_update_by' do
diff_position = build(:diff_position) context 'when a diff note' do
line_code = 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521' let(:note) { create(:diff_note_on_merge_request) }
diff_note_position = build(:diff_note_position, line_code: line_code, position: diff_position) let(:diff_position) { build(:diff_position) }
let(:line_code) { 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521' }
expect(diff_note_position.position).to eq(diff_position) let(:diff_note_position) { note.diff_note_positions.first }
expect(diff_note_position.line_code).to eq(line_code) let(:params) { { diff_type: :head, line_code: line_code, position: diff_position } }
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 end
it 'unique by note_id and diff type' do it 'unique by note_id and diff type' do
......
# 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
# 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
...@@ -33,6 +33,24 @@ describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shared_sta ...@@ -33,6 +33,24 @@ describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shared_sta
expect(merge_request.merge_status).to eq('can_be_merged') expect(merge_request.merge_status).to eq('can_be_merged')
end 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 it 'updates the merge ref' do
expect { subject }.to change(merge_request, :merge_ref_head).from(nil) expect { subject }.to change(merge_request, :merge_ref_head).from(nil)
end end
......
...@@ -143,10 +143,21 @@ describe Notes::CreateService do ...@@ -143,10 +143,21 @@ describe Notes::CreateService do
end end
it 'note is associated with a note diff file' do 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 note = described_class.new(project_with_repo, user, new_opts).execute
expect(note).to be_persisted expect(note).to be_persisted
expect(note.note_diff_file).to be_present 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
end end
...@@ -160,6 +171,8 @@ describe Notes::CreateService do ...@@ -160,6 +171,8 @@ describe Notes::CreateService do
end end
it 'note is not associated with a note diff file' do 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 note = described_class.new(project_with_repo, user, new_opts).execute
expect(note).to be_persisted expect(note).to be_persisted
......
...@@ -73,7 +73,9 @@ module TestEnv ...@@ -73,7 +73,9 @@ module TestEnv
'submodule_inside_folder' => 'b491b92', 'submodule_inside_folder' => 'b491b92',
'png-lfs' => 'fe42f41', 'png-lfs' => 'fe42f41',
'sha-starting-with-large-number' => '8426165', '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 }.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
......
...@@ -38,15 +38,12 @@ module ImportExport ...@@ -38,15 +38,12 @@ module ImportExport
end end
def setup_reader(reader) def setup_reader(reader)
case reader if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson)
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
allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(false) 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) allow_any_instance_of(Gitlab::ImportExport::JSON::NdjsonReader).to receive(:exist?).and_return(true)
else 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
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment