Commit c1924b86 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 6315ed96
...@@ -71,6 +71,7 @@ export const getFileData = ( ...@@ -71,6 +71,7 @@ export const getFileData = (
const url = joinPaths( const url = joinPaths(
gon.relative_url_root || '/', gon.relative_url_root || '/',
state.currentProjectId, state.currentProjectId,
'-',
file.type, file.type,
getters.lastCommit && getters.lastCommit.id, getters.lastCommit && getters.lastCommit.id,
escapeFileUrl(file.prevPath || file.path), escapeFileUrl(file.prevPath || file.path),
......
...@@ -27,7 +27,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) { ...@@ -27,7 +27,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
fetchpromise = axios fetchpromise = axios
.get( .get(
`${gon.relative_url_root}/${projectPath}/refs/${ref}/logs_tree/${path.replace(/^\//, '')}`, `${gon.relative_url_root}/${projectPath}/-/refs/${ref}/logs_tree/${path.replace(/^\//, '')}`,
{ {
params: { format: 'json', offset }, params: { format: 'json', offset },
}, },
......
.snippet-row { .snippet-row {
.title { .title {
margin-bottom: 2px; margin-bottom: 2px;
font-weight: $gl-font-weight-bold;
} }
.snippet-filename { .snippet-filename {
...@@ -11,6 +12,10 @@ ...@@ -11,6 +12,10 @@
.snippet-info { .snippet-info {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
a {
color: $gl-text-color;
}
} }
.snippet-form-holder .file-holder .file-title { .snippet-form-holder .file-holder .file-title {
......
...@@ -30,7 +30,7 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle ...@@ -30,7 +30,7 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle
service = ErrorTracking::IssueUpdateService.new(project, current_user, issue_update_params) service = ErrorTracking::IssueUpdateService.new(project, current_user, issue_update_params)
result = service.execute result = service.execute
return if handle_errors(result) return if render_errors(result)
render json: { render json: {
result: result result: result
...@@ -47,7 +47,7 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle ...@@ -47,7 +47,7 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle
) )
result = service.execute result = service.execute
return if handle_errors(result) return if render_errors(result)
render json: { render json: {
errors: serialize_errors(result[:issues]), errors: serialize_errors(result[:issues]),
...@@ -60,14 +60,14 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle ...@@ -60,14 +60,14 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle
service = ErrorTracking::IssueDetailsService.new(project, current_user, issue_details_params) service = ErrorTracking::IssueDetailsService.new(project, current_user, issue_details_params)
result = service.execute result = service.execute
return if handle_errors(result) return if render_errors(result)
render json: { render json: {
error: serialize_detailed_error(result[:issue]) error: serialize_detailed_error(result[:issue])
} }
end end
def handle_errors(result) def render_errors(result)
unless result[:status] == :success unless result[:status] == :success
render json: { message: result[:message] }, render json: { message: result[:message] },
status: result[:http_status] || :bad_request status: result[:http_status] || :bad_request
...@@ -75,7 +75,7 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle ...@@ -75,7 +75,7 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle
end end
def list_issues_params def list_issues_params
params.permit(:search_term, :sort, :cursor) params.permit(:search_term, :sort, :cursor, :issue_status)
end end
def issue_update_params def issue_update_params
......
...@@ -89,8 +89,10 @@ module ErrorTracking ...@@ -89,8 +89,10 @@ module ErrorTracking
end end
def list_sentry_projects def list_sentry_projects
handle_exceptions do
{ projects: sentry_client.projects } { projects: sentry_client.projects }
end end
end
def issue_details(opts = {}) def issue_details(opts = {})
with_reactive_cache('issue_details', opts.stringify_keys) do |result| with_reactive_cache('issue_details', opts.stringify_keys) do |result|
......
...@@ -3,21 +3,9 @@ ...@@ -3,21 +3,9 @@
module ErrorTracking module ErrorTracking
class BaseService < ::BaseService class BaseService < ::BaseService
def execute def execute
unauthorized = check_permissions
return unauthorized if unauthorized return unauthorized if unauthorized
begin perform
response = perform
rescue Sentry::Client::Error => e
return error(e.message, :bad_request)
rescue Sentry::Client::MissingKeysError => e
return error(e.message, :internal_server_error)
end
errors = parse_errors(response)
return errors if errors
success(parse_response(response))
end end
private private
...@@ -27,12 +15,21 @@ module ErrorTracking ...@@ -27,12 +15,21 @@ module ErrorTracking
"#{self.class} does not implement #{__method__}" "#{self.class} does not implement #{__method__}"
end end
def compose_response(response, &block)
errors = parse_errors(response)
return errors if errors
yield if block_given?
success(parse_response(response))
end
def parse_response(response) def parse_response(response)
raise NotImplementedError, raise NotImplementedError,
"#{self.class} does not implement #{__method__}" "#{self.class} does not implement #{__method__}"
end end
def check_permissions def unauthorized
return error('Error Tracking is not enabled') unless enabled? return error('Error Tracking is not enabled') unless enabled?
return error('Access denied', :unauthorized) unless can_read? return error('Access denied', :unauthorized) unless can_read?
end end
......
...@@ -5,7 +5,9 @@ module ErrorTracking ...@@ -5,7 +5,9 @@ module ErrorTracking
private private
def perform def perform
project_error_tracking_setting.issue_details(issue_id: params[:issue_id]) response = project_error_tracking_setting.issue_details(issue_id: params[:issue_id])
compose_response(response)
end end
def parse_response(response) def parse_response(response)
......
...@@ -5,7 +5,9 @@ module ErrorTracking ...@@ -5,7 +5,9 @@ module ErrorTracking
private private
def perform def perform
project_error_tracking_setting.issue_latest_event(issue_id: params[:issue_id]) response = project_error_tracking_setting.issue_latest_event(issue_id: params[:issue_id])
compose_response(response)
end end
def parse_response(response) def parse_response(response)
......
...@@ -7,21 +7,15 @@ module ErrorTracking ...@@ -7,21 +7,15 @@ module ErrorTracking
private private
def perform def perform
response = fetch response = project_error_tracking_setting.update_issue(
issue_id: params[:issue_id],
params: update_params
)
unless parse_errors(response).present? compose_response(response) do
response[:closed_issue_iid] = update_related_issue&.iid response[:closed_issue_iid] = update_related_issue&.iid
project_error_tracking_setting.expire_issues_cache project_error_tracking_setting.expire_issues_cache
end end
response
end
def fetch
project_error_tracking_setting.update_issue(
issue_id: params[:issue_id],
params: update_params
)
end end
def update_related_issue def update_related_issue
...@@ -74,7 +68,7 @@ module ErrorTracking ...@@ -74,7 +68,7 @@ module ErrorTracking
} }
end end
def check_permissions def unauthorized
return error('Error Tracking is not enabled') unless enabled? return error('Error Tracking is not enabled') unless enabled?
return error('Access denied', :unauthorized) unless can_update? return error('Access denied', :unauthorized) unless can_update?
end end
......
...@@ -6,6 +6,13 @@ module ErrorTracking ...@@ -6,6 +6,13 @@ module ErrorTracking
DEFAULT_LIMIT = 20 DEFAULT_LIMIT = 20
DEFAULT_SORT = 'last_seen' DEFAULT_SORT = 'last_seen'
# Sentry client supports 'muted' and 'assigned' but GitLab does not
ISSUE_STATUS_VALUES = %w[
resolved
unresolved
ignored
].freeze
def external_url def external_url
project_error_tracking_setting&.sentry_external_url project_error_tracking_setting&.sentry_external_url
end end
...@@ -13,19 +20,31 @@ module ErrorTracking ...@@ -13,19 +20,31 @@ module ErrorTracking
private private
def perform def perform
project_error_tracking_setting.list_sentry_issues( return invalid_status_error unless valid_status?
response = project_error_tracking_setting.list_sentry_issues(
issue_status: issue_status, issue_status: issue_status,
limit: limit, limit: limit,
search_term: params[:search_term].presence, search_term: params[:search_term].presence,
sort: sort, sort: sort,
cursor: params[:cursor].presence cursor: params[:cursor].presence
) )
compose_response(response)
end end
def parse_response(response) def parse_response(response)
response.slice(:issues, :pagination) response.slice(:issues, :pagination)
end end
def invalid_status_error
error('Bad Request: Invalid issue_status', http_status_for(:bad_Request))
end
def valid_status?
ISSUE_STATUS_VALUES.include?(issue_status)
end
def issue_status def issue_status
params[:issue_status] || DEFAULT_ISSUE_STATUS params[:issue_status] || DEFAULT_ISSUE_STATUS
end end
......
...@@ -2,18 +2,16 @@ ...@@ -2,18 +2,16 @@
module ErrorTracking module ErrorTracking
class ListProjectsService < ErrorTracking::BaseService class ListProjectsService < ErrorTracking::BaseService
def execute private
def perform
unless project_error_tracking_setting.valid? unless project_error_tracking_setting.valid?
return error(project_error_tracking_setting.errors.full_messages.join(', '), :bad_request) return error(project_error_tracking_setting.errors.full_messages.join(', '), :bad_request)
end end
super response = project_error_tracking_setting.list_sentry_projects
end
private compose_response(response)
def perform
project_error_tracking_setting.list_sentry_projects
end end
def parse_response(response) def parse_response(response)
......
...@@ -3,17 +3,22 @@ ...@@ -3,17 +3,22 @@
- snippet_chunks = snippet_blob[:snippet_chunks] - snippet_chunks = snippet_blob[:snippet_chunks]
- snippet_path = gitlab_snippet_path(snippet) - snippet_path = gitlab_snippet_path(snippet)
.search-result-row .search-result-row.snippet-row
%span = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
.title
= link_to gitlab_snippet_path(snippet) do
= snippet.title = snippet.title
.snippet-info
= snippet.to_reference
&middot;
authored
= time_ago_with_tooltip(snippet.created_at)
by by
= link_to user_snippets_path(snippet.author) do = link_to user_snippets_path(snippet.author) do
= image_tag avatar_icon_for_user(snippet.author), class: "avatar avatar-inline s16", alt: ''
= snippet.author_name = snippet.author_name
%span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title .file-holder.my-2
.file-holder .js-file-title.file-title-flex-parent
.js-file-title.file-title
= link_to snippet_path do = link_to snippet_path do
%i.fa.fa-file %i.fa.fa-file
%strong= snippet.file_name %strong= snippet.file_name
......
---
title: Fix design of snippet search results page
merge_request: 23780
author:
type: fixed
---
title: Migrate epic, epic notes mentions to respective DB table
merge_request: 22333
author:
type: changed
---
title: Fix pipeline status loading errors on project dashboard page caused by Gitaly
connection failures
merge_request: 23378
author:
type: fixed
# frozen_string_literal: true
class MigrateEpicMentionsToDb < ActiveRecord::Migration[5.2]
DOWNTIME = false
disable_ddl_transaction!
DELAY = 2.minutes.to_i
BATCH_SIZE = 10000
MIGRATION = 'UserMentions::CreateResourceUserMention'
JOIN = "LEFT JOIN epic_user_mentions on epics.id = epic_user_mentions.epic_id"
QUERY_CONDITIONS = "(description like '%@%' OR title like '%@%') AND epic_user_mentions.epic_id is null"
class Epic < ActiveRecord::Base
include EachBatch
self.table_name = 'epics'
end
def up
return unless Gitlab.ee?
Epic
.joins(JOIN)
.where(QUERY_CONDITIONS)
.each_batch(of: BATCH_SIZE) do |batch, index|
range = batch.pluck(Arel.sql('MIN(epics.id)'), Arel.sql('MAX(epics.id)')).first
BackgroundMigrationWorker.perform_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, false, *range])
end
end
def down
# no-op
end
end
# frozen_string_literal: true
class MigrateEpicNotesMentionsToDb < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
DELAY = 2.minutes.to_i
BATCH_SIZE = 10000
MIGRATION = 'UserMentions::CreateResourceUserMention'
INDEX_NAME = 'epic_mentions_temp_index'
INDEX_CONDITION = "note LIKE '%@%'::text AND notes.noteable_type = 'Epic'"
QUERY_CONDITIONS = "#{INDEX_CONDITION} AND epic_user_mentions.epic_id IS NULL"
JOIN = 'LEFT JOIN epic_user_mentions ON notes.id = epic_user_mentions.note_id'
class Note < ActiveRecord::Base
include EachBatch
self.table_name = 'notes'
end
def up
return unless Gitlab.ee?
# create temporary index for notes with mentions, may take well over 1h
add_concurrent_index(:notes, :id, where: INDEX_CONDITION, name: INDEX_NAME)
Note
.joins(JOIN)
.where(QUERY_CONDITIONS)
.each_batch(of: BATCH_SIZE) do |batch, index|
range = batch.pluck(Arel.sql('MIN(notes.id)'), Arel.sql('MAX(notes.id)')).first
BackgroundMigrationWorker.perform_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, true, *range])
end
end
def down
# no-op
# temporary index is to be dropped in a different migration in an upcoming release:
# https://gitlab.com/gitlab-org/gitlab/issues/196842
end
end
...@@ -2778,7 +2778,6 @@ ActiveRecord::Schema.define(version: 2020_01_27_090233) do ...@@ -2778,7 +2778,6 @@ ActiveRecord::Schema.define(version: 2020_01_27_090233) do
t.index ["commit_id"], name: "index_notes_on_commit_id" t.index ["commit_id"], name: "index_notes_on_commit_id"
t.index ["created_at"], name: "index_notes_on_created_at" t.index ["created_at"], name: "index_notes_on_created_at"
t.index ["discussion_id"], name: "index_notes_on_discussion_id" t.index ["discussion_id"], name: "index_notes_on_discussion_id"
t.index ["id"], name: "epic_mentions_temp_index", where: "((note ~~ '%@%'::text) AND ((noteable_type)::text = 'Epic'::text))"
t.index ["line_code"], name: "index_notes_on_line_code" t.index ["line_code"], name: "index_notes_on_line_code"
t.index ["note"], name: "index_notes_on_note_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["note"], name: "index_notes_on_note_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type" t.index ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type"
......
...@@ -9,20 +9,29 @@ description: 'Learn how to contribute to GitLab.' ...@@ -9,20 +9,29 @@ description: 'Learn how to contribute to GitLab.'
- Set up GitLab's development environment with [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md) - Set up GitLab's development environment with [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md)
- [GitLab contributing guide](contributing/index.md) - [GitLab contributing guide](contributing/index.md)
- [Architecture](architecture.md) of GitLab - [Issues workflow](contributing/issue_workflow.md) (issue tracker guidelines, triaging, labels, feature proposals, issue weight, regression issues, technical and UX debt)
- [Merge requests workflow](contributing/merge_request_workflow.md) (merge request guidelines, contribution acceptance criteria, definition of done, dependencies)
- [Style guides](contributing/style_guides.md)
- [Implement design & UI elements](contributing/design.md)
- [GitLab Architecture Overview](architecture.md)
- [Rake tasks](rake_tasks.md) for development - [Rake tasks](rake_tasks.md) for development
## Processes ## Processes
- [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab/blob/master/PROCESS.md) **Must-reads:**
- [Generate a changelog entry with `bin/changelog`](changelog.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed - [Code review guidelines](code_review.md) for reviewing code and having code reviewed
- [Database review guidelines](database_review.md) for reviewing database-related changes and complex SQL queries, and having them reviewed - [Database review guidelines](database_review.md) for reviewing database-related changes and complex SQL queries, and having them reviewed
- [Pipelines for the GitLab project](pipelines.md) - [Pipelines for the GitLab project](pipelines.md)
- [Guidelines for implementing Enterprise Edition features](ee_features.md)
Complementary reads:
- [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab/blob/master/PROCESS.md)
- [Security process for developers](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#security-releases-critical-non-critical-as-a-developer) - [Security process for developers](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#security-releases-critical-non-critical-as-a-developer)
- [Requesting access to Chatops on GitLab.com](chatops_on_gitlabcom.md#requesting-access) (for GitLabbers) - [Guidelines for implementing Enterprise Edition features](ee_features.md)
- [Danger bot](dangerbot.md) - [Danger bot](dangerbot.md)
- [Generate a changelog entry with `bin/changelog`](changelog.md)
- [Requesting access to Chatops on GitLab.com](chatops_on_gitlabcom.md#requesting-access) (for GitLabbers)
## UX and Frontend guides ## UX and Frontend guides
......
...@@ -118,6 +118,11 @@ This [documentation](merge_request_workflow.md) outlines the current merge reque ...@@ -118,6 +118,11 @@ This [documentation](merge_request_workflow.md) outlines the current merge reque
This [documentation](style_guides.md) outlines the current style guidelines. This [documentation](style_guides.md) outlines the current style guidelines.
## Implement design & UI elements
This [design documentation](design.md) outlines the current process for implementing
design & UI elements.
## Getting an Enterprise Edition License ## Getting an Enterprise Edition License
If you need a license for contributing to an EE-feature, please [follow these instructions](https://about.gitlab.com/handbook/marketing/community-relations/code-contributor-program/#for-contributors-to-the-gitlab-enterprise-edition-ee). If you need a license for contributing to an EE-feature, please [follow these instructions](https://about.gitlab.com/handbook/marketing/community-relations/code-contributor-program/#for-contributors-to-the-gitlab-enterprise-edition-ee).
......
# Style guides # Style guides
1. [Ruby](https://github.com/rubocop-hq/ruby-style-guide). ## Pre-commit static analysis
Important sections include [Source Code Layout][rss-source] and
[Naming][rss-naming]. Use: You're strongly advised to install
- multi-line method chaining style **Option A**: dot `.` on the second line [Overcommit](https://github.com/sds/overcommit) to automatically check for
- string literal quoting style **Option A**: single quoted by default static analysis offenses before committing locally:
1. [Rails](https://github.com/rubocop-hq/rails-style-guide)
1. [Newlines styleguide][newlines-styleguide] ```shell
1. [Testing][testing] cd tooling/overcommit && make && cd -
1. [JavaScript styleguide][js-styleguide] ```
1. [SCSS styleguide][scss-styleguide]
1. [Shell commands (Ruby)](../shell_commands.md) created by GitLab Then before a commit is created, Overcommit will automatically check for
contributors to enhance security RuboCop (and other checks) offenses on every modified file.
1. [Database Migrations](../migration_style_guide.md)
1. [Markdown](https://cirosantilli.com/markdown-style-guide/) This saves you time as you don't have to wait for the same errors to be detected
1. [Documentation styleguide](../documentation/styleguide.md) by the CI.
1. Interface text should be written subjectively instead of objectively. It
should be the GitLab core team addressing a person. It should be written in ## Ruby, Rails, RSpec
present time and never use past tense (has been/was). For example instead
of _prohibited this user from being saved due to the following errors:_ the Our codebase style is defined and enforced by [RuboCop](https://github.com/rubocop-hq/rubocop).
text should be _sorry, we could not create your account because:_
1. Code should be written in [US English][us-english] You can check for any offenses locally with `bundle exec rubocop --parallel`.
1. [Go](../go_guide/index.md) On the CI, this is automatically checked by the `static-analysis` jobs.
1. [Python](../python_guide/index.md)
1. [Shell scripting](../shell_scripting_guide/index.md) For RuboCop rules that we have not taken a decision yet, we follow the
[Ruby Style Guide](https://github.com/rubocop-hq/ruby-style-guide),
## Checking the style and other issues [Rails Style Guide](https://github.com/rubocop-hq/rails-style-guide), and
[RSpec Style Guide](https://github.com/rubocop-hq/rspec-style-guide) as general
This is also the style used by linting tools such as guidelines to write idiomatic Ruby/Rails/RSpec, but reviewers/maintainers should
[RuboCop](https://github.com/rubocop-hq/rubocop) and [Hound CI](https://houndci.com). be tolerant and not too pedantic about style.
You can run RuboCop by hand or install a tool like [Overcommit](https://github.com/sds/overcommit) to run it for you.
Similarly, some RuboCop rules are currently disabled, and for those,
Overcommit will automatically run the configured checks (like Rubocop) on every modified file before commit. reviewers/maintainers must not ask authors to use one style or the other, as both
You can use the example overcommit configuration found in `.overcommit.yml.example` as a quickstart. are accepted. This isn't an ideal situation since this leaves space for
This saves you time as you don't have to wait for the same errors to be detected by the CI. [bike-shedding](https://en.wiktionary.org/wiki/bikeshedding), and ideally we
should enable all RuboCop rules to avoid style-related
discussions/nitpicking/back-and-forth in reviews.
Additionally, we have a dedicated
[newlines styleguide](../newlines_styleguide.md), as well as dedicated
[test-specific style guides and best practices](../testing_guide/index.md).
## Database migrations
See the dedicated [Database Migrations Style Guide](../migration_style_guide.md).
## JavaScript
See the dedicated [JS Style Guide](../fe_guide/style/javascript.md).
## SCSS
See the dedicated [SCSS Style Guide](../fe_guide/style/scss.md).
## Go
See the dedicated [Go standards and style guidelines](../go_guide/index.md).
## Shell commands (Ruby)
See the dedicated [Guidelines for shell commands in the GitLab codebase](../shell_commands.md).
## Shell scripting
See the dedicated [Shell scripting standards and style guidelines](../shell_scripting_guide/index.md).
## Markdown
We're following [Ciro Santilli's Markdown Style Guide](https://cirosantilli.com/markdown-style-guide).
## Documentation
See the dedicated [Documentation Style Guide](../documentation/styleguide.md).
## Python
See the dedicated [Python Development Guidelines](../python_guide/index.md).
## Misc
Code should be written in [US English](https://en.wikipedia.org/wiki/American_English).
--- ---
[Return to Contributing documentation](index.md) [Return to Contributing documentation](index.md)
[rss-source]: https://github.com/rubocop-hq/ruby-style-guide/blob/master/README.adoc#source-code-layout
[rss-naming]: https://github.com/rubocop-hq/ruby-style-guide/blob/master/README.adoc#naming-conventions
[doc-guidelines]: ../documentation/index.md "Documentation guidelines"
[js-styleguide]: ../fe_guide/style/javascript.md "JavaScript styleguide"
[scss-styleguide]: ../fe_guide/style/scss.md "SCSS styleguide"
[newlines-styleguide]: ../newlines_styleguide.md "Newlines styleguide"
[testing]: ../testing_guide/index.md
[us-english]: https://en.wikipedia.org/wiki/American_English
...@@ -4,7 +4,7 @@ The GitLab CI pipeline includes a `danger-review` job that uses [Danger](https:/ ...@@ -4,7 +4,7 @@ The GitLab CI pipeline includes a `danger-review` job that uses [Danger](https:/
to perform a variety of automated checks on the code under test. to perform a variety of automated checks on the code under test.
Danger is a gem that runs in the CI environment, like any other analysis tool. Danger is a gem that runs in the CI environment, like any other analysis tool.
What sets it apart from, e.g., Rubocop, is that it's designed to allow you to What sets it apart from, e.g., RuboCop, is that it's designed to allow you to
easily write arbitrary code to test properties of your code or changes. To this easily write arbitrary code to test properties of your code or changes. To this
end, it provides a set of common helpers and access to information about what end, it provides a set of common helpers and access to information about what
has actually changed in your environment, then simply runs your code! has actually changed in your environment, then simply runs your code!
...@@ -13,6 +13,14 @@ If Danger is asking you to change something about your merge request, it's best ...@@ -13,6 +13,14 @@ If Danger is asking you to change something about your merge request, it's best
just to make the change. If you want to learn how Danger works, or make changes just to make the change. If you want to learn how Danger works, or make changes
to the existing rules, then this is the document for you. to the existing rules, then this is the document for you.
## Run Danger locally
A subset of the current checks can be run locally with the following rake task:
```shell
bundle exec danger_local
```
## Operation ## Operation
On startup, Danger reads a [`Dangerfile`](https://gitlab.com/gitlab-org/gitlab/blob/master/Dangerfile) On startup, Danger reads a [`Dangerfile`](https://gitlab.com/gitlab-org/gitlab/blob/master/Dangerfile)
...@@ -118,7 +126,6 @@ at GitLab so far: ...@@ -118,7 +126,6 @@ at GitLab so far:
## Limitations ## Limitations
- [`danger local` does not work on GitLab](https://github.com/danger/danger/issues/458)
- Danger output is not added to a merge request comment if working on - Danger output is not added to a merge request comment if working on
a fork. This happens because the secret variable from the canonical a fork. This happens because the secret variable from the canonical
project is not shared to forks. project is not shared to forks.
......
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
module UserMentions
class CreateResourceUserMention
# Resources that have mentions to be migrated:
# issue, merge_request, epic, commit, snippet, design
BULK_INSERT_SIZE = 5000
ISOLATION_MODULE = 'Gitlab::BackgroundMigration::UserMentions::Models'
def perform(resource_model, join, conditions, with_notes, start_id, end_id)
resource_model = "#{ISOLATION_MODULE}::#{resource_model}".constantize if resource_model.is_a?(String)
model = with_notes ? "#{ISOLATION_MODULE}::Note".constantize : resource_model
resource_user_mention_model = resource_model.user_mention_model
records = model.joins(join).where(conditions).where(id: start_id..end_id)
records.in_groups_of(BULK_INSERT_SIZE, false).each do |records|
mentions = []
records.each do |record|
mentions << record.build_mention_values
end
no_quote_columns = [:note_id]
no_quote_columns << resource_user_mention_model.resource_foreign_key
Gitlab::Database.bulk_insert(
resource_user_mention_model.table_name,
mentions,
return_ids: true,
disable_quote: no_quote_columns,
on_conflict: :do_nothing
)
end
end
end
end
end
end
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
module UserMentions
module Models
class Epic < ActiveRecord::Base
include IsolatedMentionable
include CacheMarkdownField
attr_mentionable :title, pipeline: :single_line
attr_mentionable :description
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_state_filter_enabled: true
self.table_name = 'epics'
belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :group
def self.user_mention_model
Gitlab::BackgroundMigration::UserMentions::Models::EpicUserMention
end
def user_mention_model
self.class.user_mention_model
end
def project
nil
end
def mentionable_params
{ group: group, label_url_method: :group_epics_url }
end
def user_mention_resource_id
id
end
def user_mention_note_id
'NULL'
end
end
end
end
end
end
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
module UserMentions
module Models
class EpicUserMention < ActiveRecord::Base
self.table_name = 'epic_user_mentions'
def self.resource_foreign_key
:epic_id
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module UserMentions
module Models
# == IsolatedMentionable concern
#
# Shortcutted for isolation version of Mentionable to be used in mentions migrations
#
module IsolatedMentionable
extend ::ActiveSupport::Concern
class_methods do
# Indicate which attributes of the Mentionable to search for GFM references.
def attr_mentionable(attr, options = {})
attr = attr.to_s
mentionable_attrs << [attr, options]
end
end
included do
# Accessor for attributes marked mentionable.
cattr_accessor :mentionable_attrs, instance_accessor: false do
[]
end
if self < Participable
participant -> (user, ext) { all_references(user, extractor: ext) }
end
end
def all_references(current_user = nil, extractor: nil)
# Use custom extractor if it's passed in the function parameters.
if extractor
extractors[current_user] = extractor
else
extractor = extractors[current_user] ||= ::Gitlab::ReferenceExtractor.new(project, current_user)
extractor.reset_memoized_values
end
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend
options = options.merge(
cache_key: [self, attr],
author: author,
skip_project_check: skip_project_check?
).merge(mentionable_params)
cached_html = self.try(:updated_cached_html_for, attr.to_sym)
options[:rendered] = cached_html if cached_html
extractor.analyze(text, options)
end
extractor
end
def extractors
@extractors ||= {}
end
def skip_project_check?
false
end
def build_mention_values
refs = all_references(author)
{
"#{self.user_mention_model.resource_foreign_key}": user_mention_resource_id,
note_id: user_mention_note_id,
mentioned_users_ids: array_to_sql(refs.mentioned_users.pluck(:id)),
mentioned_projects_ids: array_to_sql(refs.mentioned_projects.pluck(:id)),
mentioned_groups_ids: array_to_sql(refs.mentioned_groups.pluck(:id))
}
end
def array_to_sql(ids_array)
return unless ids_array.present?
'{' + ids_array.join(", ") + '}'
end
private
def mentionable_params
{}
end
end
end
end
end
end
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
module UserMentions
module Models
class Note < ActiveRecord::Base
include IsolatedMentionable
include CacheMarkdownField
self.table_name = 'notes'
self.inheritance_column = :_type_disabled
attr_mentionable :note, pipeline: :note
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
belongs_to :author, class_name: "User"
belongs_to :noteable, polymorphic: true
belongs_to :project
def user_mention_model
"#{CreateResourceUserMention::ISOLATION_MODULE}::#{noteable.class}".constantize.user_mention_model
end
def for_personal_snippet?
noteable.class.name == 'PersonalSnippet'
end
def for_project_noteable?
!for_personal_snippet?
end
def skip_project_check?
!for_project_noteable?
end
def for_epic?
noteable.class.name == 'Epic'
end
def user_mention_resource_id
noteable_id || commit_id
end
def user_mention_note_id
id
end
private
def mentionable_params
return super unless for_epic?
super.merge(banzai_context_params)
end
def banzai_context_params
{ group: noteable.group, label_url_method: :group_epics_url }
end
end
end
end
end
end
...@@ -59,6 +59,10 @@ module Gitlab ...@@ -59,6 +59,10 @@ module Gitlab
end end
self.loaded = true self.loaded = true
rescue GRPC::Unavailable, GRPC::DeadlineExceeded => e
# Handle Gitaly connection issues gracefully
Gitlab::ErrorTracking
.track_exception(e, project_id: project.id)
end end
def load_from_project def load_from_project
......
...@@ -251,7 +251,7 @@ describe('IDE store file actions', () => { ...@@ -251,7 +251,7 @@ describe('IDE store file actions', () => {
describe('success', () => { describe('success', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`).replyOnce( mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/${localFile.path}`).replyOnce(
200, 200,
{ {
blame_path: 'blame_path', blame_path: 'blame_path',
...@@ -273,7 +273,7 @@ describe('IDE store file actions', () => { ...@@ -273,7 +273,7 @@ describe('IDE store file actions', () => {
.dispatch('getFileData', { path: localFile.path }) .dispatch('getFileData', { path: localFile.path })
.then(() => { .then(() => {
expect(service.getFileData).toHaveBeenCalledWith( expect(service.getFileData).toHaveBeenCalledWith(
`${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`, `${RELATIVE_URL_ROOT}/test/test/-/7297abc/${localFile.path}`,
); );
done(); done();
...@@ -345,7 +345,7 @@ describe('IDE store file actions', () => { ...@@ -345,7 +345,7 @@ describe('IDE store file actions', () => {
localFile.path = 'new-shiny-file'; localFile.path = 'new-shiny-file';
store.state.entries[localFile.path] = localFile; store.state.entries[localFile.path] = localFile;
mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/old-dull-file`).replyOnce( mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/old-dull-file`).replyOnce(
200, 200,
{ {
blame_path: 'blame_path', blame_path: 'blame_path',
...@@ -376,7 +376,7 @@ describe('IDE store file actions', () => { ...@@ -376,7 +376,7 @@ describe('IDE store file actions', () => {
describe('error', () => { describe('error', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`).networkError(); mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/${localFile.path}`).networkError();
}); });
it('dispatches error action', () => { it('dispatches error action', () => {
......
...@@ -71,7 +71,7 @@ describe('fetchLogsTree', () => { ...@@ -71,7 +71,7 @@ describe('fetchLogsTree', () => {
it('calls axios get', () => it('calls axios get', () =>
fetchLogsTree(client, '', '0', resolver).then(() => { fetchLogsTree(client, '', '0', resolver).then(() => {
expect(axios.get).toHaveBeenCalledWith('/gitlab-org/gitlab-foss/refs/master/logs_tree/', { expect(axios.get).toHaveBeenCalledWith('/gitlab-org/gitlab-foss/-/refs/master/logs_tree/', {
params: { format: 'json', offset: '0' }, params: { format: 'json', offset: '0' },
}); });
})); }));
......
...@@ -114,6 +114,24 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do ...@@ -114,6 +114,24 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do
pipeline_status.load_status pipeline_status.load_status
pipeline_status.load_status pipeline_status.load_status
end end
it 'handles Gitaly unavailable exceptions gracefully' do
allow(pipeline_status).to receive(:commit).and_raise(GRPC::Unavailable)
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
an_instance_of(GRPC::Unavailable), project_id: project.id
)
expect { pipeline_status.load_status }.not_to raise_error
end
it 'handles Gitaly timeout exceptions gracefully' do
allow(pipeline_status).to receive(:commit).and_raise(GRPC::DeadlineExceeded)
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
an_instance_of(GRPC::DeadlineExceeded), project_id: project.id
)
expect { pipeline_status.load_status }.not_to raise_error
end
end end
describe "#load_from_project", :clean_gitlab_redis_cache do describe "#load_from_project", :clean_gitlab_redis_cache do
......
...@@ -190,23 +190,23 @@ describe 'project routing' do ...@@ -190,23 +190,23 @@ describe 'project routing' do
end end
it 'to #archive_alternative' do it 'to #archive_alternative' do
expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', append_sha: true) expect(get('/gitlab/gitlabhq/-/repository/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', append_sha: true)
end end
it 'to #archive_deprecated' do it 'to #archive_deprecated' do
expect(get('/gitlab/gitlabhq/repository/master/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', append_sha: true) expect(get('/gitlab/gitlabhq/-/repository/master/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', append_sha: true)
end end
it 'to #archive_deprecated format:zip' do it 'to #archive_deprecated format:zip' do
expect(get('/gitlab/gitlabhq/repository/master/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip', id: 'master', append_sha: true) expect(get('/gitlab/gitlabhq/-/repository/master/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip', id: 'master', append_sha: true)
end end
it 'to #archive_deprecated format:tar.bz2' do it 'to #archive_deprecated format:tar.bz2' do
expect(get('/gitlab/gitlabhq/repository/master/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2', id: 'master', append_sha: true) expect(get('/gitlab/gitlabhq/-/repository/master/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2', id: 'master', append_sha: true)
end end
it 'to #archive_deprecated with "/" in route' do it 'to #archive_deprecated with "/" in route' do
expect(get('/gitlab/gitlabhq/repository/improve/awesome/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'improve/awesome', append_sha: true) expect(get('/gitlab/gitlabhq/-/repository/improve/awesome/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'improve/awesome', append_sha: true)
end end
end end
...@@ -269,7 +269,7 @@ describe 'project routing' do ...@@ -269,7 +269,7 @@ describe 'project routing' do
# logs_file_project_ref GET /:project_id/refs/:id/logs_tree/:path(.:format) refs#logs_tree # logs_file_project_ref GET /:project_id/refs/:id/logs_tree/:path(.:format) refs#logs_tree
describe Projects::RefsController, 'routing' do describe Projects::RefsController, 'routing' do
it 'to #switch' do it 'to #switch' do
expect(get('/gitlab/gitlabhq/refs/switch')).to route_to('projects/refs#switch', namespace_id: 'gitlab', project_id: 'gitlabhq') expect(get('/gitlab/gitlabhq/-/refs/switch')).to route_to('projects/refs#switch', namespace_id: 'gitlab', project_id: 'gitlabhq')
end end
it 'to #logs_tree' do it 'to #logs_tree' do
...@@ -695,16 +695,16 @@ describe 'project routing' do ...@@ -695,16 +695,16 @@ describe 'project routing' do
# project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/} # project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/}
describe Projects::CompareController, 'routing' do describe Projects::CompareController, 'routing' do
it 'to #index' do it 'to #index' do
expect(get('/gitlab/gitlabhq/compare')).to route_to('projects/compare#index', namespace_id: 'gitlab', project_id: 'gitlabhq') expect(get('/gitlab/gitlabhq/-/compare')).to route_to('projects/compare#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
end end
it 'to #compare' do it 'to #compare' do
expect(post('/gitlab/gitlabhq/compare')).to route_to('projects/compare#create', namespace_id: 'gitlab', project_id: 'gitlabhq') expect(post('/gitlab/gitlabhq/-/compare')).to route_to('projects/compare#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
end end
it 'to #show' do it 'to #show' do
expect(get('/gitlab/gitlabhq/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable') expect(get('/gitlab/gitlabhq/-/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable')
expect(get('/gitlab/gitlabhq/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable') expect(get('/gitlab/gitlabhq/-/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable')
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe ErrorTracking::BaseService do
describe '#compose_response' do
let(:project) { double('project') }
let(:user) { double('user') }
let(:service) { described_class.new(project, user) }
it 'returns bad_request error when response has an error key' do
data = { error: 'Unexpected Error' }
result = service.send(:compose_response, data)
expect(result[:status]).to be(:error)
expect(result[:message]).to be('Unexpected Error')
expect(result[:http_status]).to be(:bad_request)
end
it 'returns server error when response has missing key error_type' do
data = { error: 'Unexpected Error', error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS }
result = service.send(:compose_response, data)
expect(result[:status]).to be(:error)
expect(result[:message]).to be('Unexpected Error')
expect(result[:http_status]).to be(:internal_server_error)
end
it 'returns no content when response is nil' do
data = nil
result = service.send(:compose_response, data)
expect(result[:status]).to be(:error)
expect(result[:message]).to be('Not ready. Try again later')
expect(result[:http_status]).to be(:no_content)
end
context 'when result has no errors key' do
let(:data) { { thing: :cat } }
it 'raises NotImplementedError' do
expect { service.send(:compose_response, data) }
.to raise_error(NotImplementedError)
end
context 'when parse_response is implemented' do
before do
expect(service).to receive(:parse_response) do |response|
{ animal: response[:thing] }
end
end
it 'returns successful response' do
result = service.send(:compose_response, data)
expect(result[:animal]).to eq(:cat)
expect(result[:status]).to eq(:success)
end
it 'returns successful response with changes from passed block' do
result = service.send(:compose_response, data) do
data[:thing] = :fish
end
expect(result[:animal]).to eq(:fish)
expect(result[:status]).to eq(:success)
end
end
end
end
end
...@@ -20,19 +20,29 @@ describe ErrorTracking::ListIssuesService do ...@@ -20,19 +20,29 @@ describe ErrorTracking::ListIssuesService do
describe '#execute' do describe '#execute' do
context 'with authorized user' do context 'with authorized user' do
context 'when list_sentry_issues returns issues' do let(:issues) { [] }
let(:issues) { [:list, :of, :issues] }
before do described_class::ISSUE_STATUS_VALUES.each do |status|
expect(error_tracking_setting) it "returns the issues with #{status} issue_status" do
.to receive(:list_sentry_issues) params[:issue_status] = status
.with(list_sentry_issues_args) list_sentry_issues_args[:issue_status] = status
.and_return(issues: issues, pagination: {}) expect_list_sentry_issues_with(list_sentry_issues_args)
expect(result).to eq(status: :success, pagination: {}, issues: issues)
end
end end
it 'returns the issues' do it 'returns the issues with no issue_status' do
expect_list_sentry_issues_with(list_sentry_issues_args)
expect(result).to eq(status: :success, pagination: {}, issues: issues) expect(result).to eq(status: :success, pagination: {}, issues: issues)
end end
it 'returns bad request for an issue_status not on the whitelist' do
params[:issue_status] = 'assigned'
expect(error_tracking_setting).not_to receive(:list_sentry_issues)
expect(result).to eq(message: "Bad Request: Invalid issue_status", status: :error, http_status: :bad_request)
end end
include_examples 'error tracking service data not ready', :list_sentry_issues include_examples 'error tracking service data not ready', :list_sentry_issues
...@@ -52,3 +62,10 @@ describe ErrorTracking::ListIssuesService do ...@@ -52,3 +62,10 @@ describe ErrorTracking::ListIssuesService do
end end
end end
end end
def expect_list_sentry_issues_with(list_sentry_issues_args)
expect(error_tracking_setting)
.to receive(:list_sentry_issues)
.with(list_sentry_issues_args)
.and_return(issues: [], pagination: {})
end
...@@ -63,32 +63,6 @@ describe ErrorTracking::ListProjectsService do ...@@ -63,32 +63,6 @@ describe ErrorTracking::ListProjectsService do
end end
end end
context 'sentry client raises exception' do
context 'Sentry::Client::Error' do
before do
expect(error_tracking_setting).to receive(:list_sentry_projects)
.and_raise(Sentry::Client::Error, 'Sentry response status code: 500')
end
it 'returns error response' do
expect(result[:message]).to eq('Sentry response status code: 500')
expect(result[:http_status]).to eq(:bad_request)
end
end
context 'Sentry::Client::MissingKeysError' do
before do
expect(error_tracking_setting).to receive(:list_sentry_projects)
.and_raise(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
end
it 'returns error response' do
expect(result[:message]).to eq('Sentry API response is missing keys. key not found: "id"')
expect(result[:http_status]).to eq(:internal_server_error)
end
end
end
context 'with invalid url' do context 'with invalid url' do
let(:params) do let(:params) do
ActionController::Parameters.new( ActionController::Parameters.new(
......
...@@ -229,17 +229,16 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type| ...@@ -229,17 +229,16 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type|
context 'when mentionable description contains mentions' do context 'when mentionable description contains mentions' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:mentionable_desc) { "#{user.to_reference} #{user2.to_reference} #{user.to_reference} some description #{group.to_reference(full: true)} and #{user2.to_reference} @all" } let(:mentionable_desc) { "#{user.to_reference} some description #{group.to_reference(full: true)} and @all" }
let(:mentionable) { create(mentionable_type, description: mentionable_desc) } let(:mentionable) { create(mentionable_type, description: mentionable_desc) }
it 'stores mentions' do it 'stores mentions' do
add_member(user) add_member(user)
expect(mentionable.user_mentions.count).to eq 1 expect(mentionable.user_mentions.count).to eq 1
expect(mentionable.referenced_users).to match_array([user, user2]) expect(mentionable.referenced_users).to match_array([user])
expect(mentionable.referenced_projects(user)).to match_array([mentionable.project].compact) # epic.project is nil, and we want empty [] expect(mentionable.referenced_projects(user)).to match_array([mentionable.project].compact) # epic.project is nil, and we want empty []
expect(mentionable.referenced_groups(user)).to match_array([group]) expect(mentionable.referenced_groups(user)).to match_array([group])
end end
...@@ -250,9 +249,8 @@ end ...@@ -250,9 +249,8 @@ end
RSpec.shared_examples 'mentions in notes' do |mentionable_type| RSpec.shared_examples 'mentions in notes' do |mentionable_type|
context 'when mentionable notes contain mentions' do context 'when mentionable notes contain mentions' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:note_desc) { "#{user.to_reference} #{user2.to_reference} #{user.to_reference} and #{group.to_reference(full: true)} and #{user2.to_reference} @all" } let(:note_desc) { "#{user.to_reference} and #{group.to_reference(full: true)} and @all" }
let!(:mentionable) { note.noteable } let!(:mentionable) { note.noteable }
before do before do
...@@ -263,7 +261,7 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type| ...@@ -263,7 +261,7 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type|
it 'returns all mentionable mentions' do it 'returns all mentionable mentions' do
expect(mentionable.user_mentions.count).to eq 1 expect(mentionable.user_mentions.count).to eq 1
expect(mentionable.referenced_users).to eq [user, user2] expect(mentionable.referenced_users).to eq [user]
expect(mentionable.referenced_projects(user)).to eq [mentionable.project].compact # epic.project is nil, and we want empty [] expect(mentionable.referenced_projects(user)).to eq [mentionable.project].compact # epic.project is nil, and we want empty []
expect(mentionable.referenced_groups(user)).to eq [group] expect(mentionable.referenced_groups(user)).to eq [group]
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