Commit 1a1e42ad authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'rs-system-note' into 'master'

Add SystemNoteService class

The Note model was basically two models crammed together - one handling user-created notes
(i.e., comments on things) and one handling system-created notes (i.e., references).
This splits out the system-specific stuff to a new SystemNoteService class.

See merge request !595
parents ff13fb0a 54db527b
...@@ -39,7 +39,7 @@ module Mentionable ...@@ -39,7 +39,7 @@ module Mentionable
# Determine whether or not a cross-reference Note has already been created between this Mentionable and # Determine whether or not a cross-reference Note has already been created between this Mentionable and
# the specified target. # the specified target.
def has_mentioned?(target) def has_mentioned?(target)
Note.cross_reference_exists?(target, local_reference) SystemNoteService.cross_reference_exists?(target, local_reference)
end end
def mentioned_users(current_user = nil, p = project) def mentioned_users(current_user = nil, p = project)
......
...@@ -63,143 +63,9 @@ class Note < ActiveRecord::Base ...@@ -63,143 +63,9 @@ class Note < ActiveRecord::Base
after_update :set_references after_update :set_references
class << self class << self
def create_status_change_note(noteable, project, author, status, source) # TODO (rspeicher): Update usages
body = "Status changed to #{status}#{' by ' + source.gfm_reference if source}" def create_cross_reference_note(*args)
SystemNoteService.cross_reference(*args)
create(
noteable: noteable,
project: project,
author: author,
note: body,
system: true
)
end
# +noteable+ was referenced from +mentioner+, by including GFM in either
# +mentioner+'s description or an associated Note.
# Create a system Note associated with +noteable+ with a GFM back-reference
# to +mentioner+.
def create_cross_reference_note(noteable, mentioner, author)
gfm_reference = mentioner_gfm_ref(noteable, mentioner)
note_options = {
project: noteable.project,
author: author,
note: cross_reference_note_content(gfm_reference),
system: true
}
if noteable.kind_of?(Commit)
note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id)
else
note_options.merge!(noteable: noteable)
end
create(note_options) unless cross_reference_disallowed?(noteable, mentioner)
end
def create_milestone_change_note(noteable, project, author, milestone)
body = if milestone.nil?
'Milestone removed'
else
"Milestone changed to #{milestone.title}"
end
create(
noteable: noteable,
project: project,
author: author,
note: body,
system: true
)
end
def create_assignee_change_note(noteable, project, author, assignee)
body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}"
create({
noteable: noteable,
project: project,
author: author,
note: body,
system: true
})
end
def create_labels_change_note(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count
added_labels = added_labels.map{ |label| "~#{label.id}" }.join(' ')
removed_labels = removed_labels.map{ |label| "~#{label.id}" }.join(' ')
message = ''
if added_labels.present?
message << "added #{added_labels}"
end
if added_labels.present? && removed_labels.present?
message << ' and '
end
if removed_labels.present?
message << "removed #{removed_labels}"
end
message << ' ' << 'label'.pluralize(labels_count)
body = "#{message.capitalize}"
create(
noteable: noteable,
project: project,
author: author,
note: body,
system: true
)
end
def create_new_commits_note(merge_request, project, author, new_commits, existing_commits = [], oldrev = nil)
total_count = new_commits.length + existing_commits.length
commits_text = ActionController::Base.helpers.pluralize(total_count, 'commit')
body = "Added #{commits_text}:\n\n"
if existing_commits.length > 0
commit_ids =
if existing_commits.length == 1
existing_commits.first.short_id
else
if oldrev
"#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
else
"#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
end
end
commits_text = ActionController::Base.helpers.pluralize(existing_commits.length, 'commit')
branch =
if merge_request.for_fork?
"#{merge_request.target_project_namespace}:#{merge_request.target_branch}"
else
merge_request.target_branch
end
message = "* #{commit_ids} - #{commits_text} from branch `#{branch}`"
body << message
body << "\n"
end
new_commits.each do |commit|
message = "* #{commit.short_id} - #{commit.title}"
body << message
body << "\n"
end
create(
noteable: merge_request,
project: project,
author: author,
note: body,
system: true
)
end end
def discussions_from_notes(notes) def discussions_from_notes(notes)
...@@ -227,88 +93,19 @@ class Note < ActiveRecord::Base ...@@ -227,88 +93,19 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym [:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end end
# Determine if cross reference note should be created.
# eg. mentioning a commit in MR comments which exists inside a MR
# should not create "mentioned in" note.
def cross_reference_disallowed?(noteable, mentioner)
if mentioner.kind_of?(MergeRequest)
mentioner.commits.map(&:id).include? noteable.id
end
end
# Determine whether or not a cross-reference note already exists.
def cross_reference_exists?(noteable, mentioner)
gfm_reference = mentioner_gfm_ref(noteable, mentioner, true)
notes = if noteable.is_a?(Commit)
where(commit_id: noteable.id, noteable_type: 'Commit')
else
where(noteable_id: noteable.id, noteable_type: noteable.class)
end
notes.where('note like ?', cross_reference_note_pattern(gfm_reference)).
system.any?
end
def search(query) def search(query)
where("note like :query", query: "%#{query}%") where("note like :query", query: "%#{query}%")
end end
def cross_reference_note_prefix
'mentioned in '
end
private
def cross_reference_note_content(gfm_reference)
cross_reference_note_prefix + "#{gfm_reference}"
end
def cross_reference_note_pattern(gfm_reference)
# Older cross reference notes contained underscores for emphasis
"%" + cross_reference_note_content(gfm_reference) + "%"
end
# Prepend the mentioner's namespaced project path to the GFM reference for
# cross-project references. For same-project references, return the
# unmodified GFM reference.
def mentioner_gfm_ref(noteable, mentioner, cross_reference = false)
if mentioner.is_a?(Commit) && cross_reference
return mentioner.gfm_reference.sub('commit ', 'commit %')
end
full_gfm_reference(mentioner.project, noteable.project, mentioner)
end end
# Return the +mentioner+ GFM reference. If the mentioner and noteable def cross_reference?
# projects are not the same, add the mentioning project's path to the system && SystemNoteService.cross_reference?(note)
# returned value.
def full_gfm_reference(mentioning_project, noteable_project, mentioner)
if mentioning_project == noteable_project
mentioner.gfm_reference
else
if mentioner.is_a?(Commit)
mentioner.gfm_reference.sub(
/(commit )/,
"\\1#{mentioning_project.path_with_namespace}@"
)
else
mentioner.gfm_reference.sub(
/(issue |merge request )/,
"\\1#{mentioning_project.path_with_namespace}"
)
end
end
end
end end
def max_attachment_size def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i current_application_settings.max_attachment_size.megabytes.to_i
end end
def cross_reference?
note.start_with?(self.class.cross_reference_note_prefix)
end
def find_diff def find_diff
return nil unless noteable && noteable.diffs.present? return nil unless noteable && noteable.diffs.present?
...@@ -449,16 +246,6 @@ class Note < ActiveRecord::Base ...@@ -449,16 +246,6 @@ class Note < ActiveRecord::Base
@discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code) @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
end end
# Returns true if this is a downvote note,
# otherwise false is returned
def downvote?
votable? && (note.start_with?('-1') ||
note.start_with?(':-1:') ||
note.start_with?(':thumbsdown:') ||
note.start_with?(':thumbs_down_sign:')
)
end
def for_commit? def for_commit?
noteable_type == "Commit" noteable_type == "Commit"
end end
...@@ -500,14 +287,18 @@ class Note < ActiveRecord::Base ...@@ -500,14 +287,18 @@ class Note < ActiveRecord::Base
nil nil
end end
# Returns true if this is an upvote note, DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:)
# otherwise false is returned
# Check if the note is a downvote
def downvote?
votable? && note.start_with?(*DOWNVOTES)
end
UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:)
# Check if the note is an upvote
def upvote? def upvote?
votable? && (note.start_with?('+1') || votable? && note.start_with?(*UPVOTES)
note.start_with?(':+1:') ||
note.start_with?(':thumbsup:') ||
note.start_with?(':thumbs_up_sign:')
)
end end
def superceded?(notes) def superceded?(notes)
......
...@@ -2,17 +2,17 @@ class IssuableBaseService < BaseService ...@@ -2,17 +2,17 @@ class IssuableBaseService < BaseService
private private
def create_assignee_note(issuable) def create_assignee_note(issuable)
Note.create_assignee_change_note( SystemNoteService.change_assignee(
issuable, issuable.project, current_user, issuable.assignee) issuable, issuable.project, current_user, issuable.assignee)
end end
def create_milestone_note(issuable) def create_milestone_note(issuable)
Note.create_milestone_change_note( SystemNoteService.change_milestone(
issuable, issuable.project, current_user, issuable.milestone) issuable, issuable.project, current_user, issuable.milestone)
end end
def create_labels_note(issuable, added_labels, removed_labels) def create_labels_note(issuable, added_labels, removed_labels)
Note.create_labels_change_note( SystemNoteService.change_label(
issuable, issuable.project, current_user, added_labels, removed_labels) issuable, issuable.project, current_user, added_labels, removed_labels)
end end
end end
...@@ -14,7 +14,7 @@ module Issues ...@@ -14,7 +14,7 @@ module Issues
private private
def create_note(issue, current_commit) def create_note(issue, current_commit)
Note.create_status_change_note(issue, issue.project, current_user, issue.state, current_commit) SystemNoteService.change_status(issue, issue.project, current_user, issue.state, current_commit)
end end
end end
end end
...@@ -14,7 +14,7 @@ module Issues ...@@ -14,7 +14,7 @@ module Issues
private private
def create_note(issue) def create_note(issue)
Note.create_status_change_note(issue, issue.project, current_user, issue.state, nil) SystemNoteService.change_status(issue, issue.project, current_user, issue.state, nil)
end end
end end
end end
...@@ -2,7 +2,7 @@ module MergeRequests ...@@ -2,7 +2,7 @@ module MergeRequests
class BaseService < ::IssuableBaseService class BaseService < ::IssuableBaseService
def create_note(merge_request) def create_note(merge_request)
Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
end end
def hook_data(merge_request, action) def hook_data(merge_request, action)
......
...@@ -82,8 +82,9 @@ module MergeRequests ...@@ -82,8 +82,9 @@ module MergeRequests
mr_commit_ids.include?(commit.id) mr_commit_ids.include?(commit.id)
end end
Note.create_new_commits_note(merge_request, merge_request.project, SystemNoteService.add_commits(merge_request, merge_request.project,
@current_user, new_commits, existing_commits, @oldrev) @current_user, new_commits,
existing_commits, @oldrev)
end end
end end
......
# SystemNoteService
#
# Used for creating system notes (e.g., when a user references a merge request
# from an issue, an issue's assignee changes, an issue is closed, etc.)
class SystemNoteService
# Called when commits are added to a Merge Request
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# new_commits - Array of Commits added since last push
# existing_commits - Array of Commits added in a previous push
# oldrev - TODO (rspeicher): I have no idea what this actually does
#
# See new_commit_summary and existing_commit_summary.
#
# Returns the created Note object
def self.add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil)
total_count = new_commits.length + existing_commits.length
commits_text = "#{total_count} commit".pluralize(total_count)
body = "Added #{commits_text}:\n\n"
body << existing_commit_summary(noteable, existing_commits, oldrev)
body << new_commit_summary(new_commits).join("\n")
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when the assignee of a Noteable is changed or removed
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# assignee - User being assigned, or nil
#
# Example Note text:
#
# "Assignee removed"
#
# "Reassigned to @rspeicher"
#
# Returns the created Note object
def self.change_assignee(noteable, project, author, assignee)
body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when one or more labels on a Noteable are added and/or removed
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# added_labels - Array of Labels added
# removed_labels - Array of Labels removed
#
# Example Note text:
#
# "Added ~1 and removed ~2 ~3 labels"
#
# "Added ~4 label"
#
# "Removed ~5 label"
#
# Returns the created Note object
def self.change_label(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count
references = ->(label) { "~#{label.id}" }
added_labels = added_labels.map(&references).join(' ')
removed_labels = removed_labels.map(&references).join(' ')
body = ''
if added_labels.present?
body << "added #{added_labels}"
body << ' and ' if removed_labels.present?
end
if removed_labels.present?
body << "removed #{removed_labels}"
end
body << ' ' << 'label'.pluralize(labels_count)
body = "#{body.capitalize}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when the milestone of a Noteable is changed
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# milestone - Milestone being assigned, or nil
#
# Example Note text:
#
# "Milestone removed"
#
# "Miletone changed to 7.11"
#
# Returns the created Note object
def self.change_milestone(noteable, project, author, milestone)
body = 'Milestone '
body += milestone.nil? ? 'removed' : "changed to #{milestone.title}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when the status of a Noteable is changed
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# status - String status
# source - Mentionable performing the change, or nil
#
# Example Note text:
#
# "Status changed to merged"
#
# "Status changed to closed by bc17db76"
#
# Returns the created Note object
def self.change_status(noteable, project, author, status, source)
body = "Status changed to #{status}"
body += " by #{source.gfm_reference}" if source
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when a Mentionable references a Noteable
#
# noteable - Noteable object being referenced
# mentioner - Mentionable object
# author - User performing the reference
#
# Example Note text:
#
# "Mentioned in #1"
#
# "Mentioned in !2"
#
# "Mentioned in 54f7727c"
#
# See cross_reference_note_content.
#
# Returns the created Note object
def self.cross_reference(noteable, mentioner, author)
return if cross_reference_disallowed?(noteable, mentioner)
gfm_reference = mentioner_gfm_ref(noteable, mentioner)
note_options = {
project: noteable.project,
author: author,
note: cross_reference_note_content(gfm_reference)
}
if noteable.kind_of?(Commit)
note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id)
else
note_options.merge!(noteable: noteable)
end
create_note(note_options)
end
def self.cross_reference?(note_text)
note_text.start_with?(cross_reference_note_prefix)
end
# Check if a cross-reference is disallowed
#
# This method prevents adding a "mentioned in !1" note on every single commit
# in a merge request.
#
# noteable - Noteable object being referenced
# mentioner - Mentionable object
#
# Returns Boolean
def self.cross_reference_disallowed?(noteable, mentioner)
return false unless MergeRequest === mentioner
return false unless Commit === noteable
mentioner.commits.include?(noteable)
end
def self.cross_reference_exists?(noteable, mentioner)
# Initial scope should be system notes of this noteable type
notes = Note.system.where(noteable_type: noteable.class)
if noteable.is_a?(Commit)
# Commits have non-integer IDs, so they're stored in `commit_id`
notes = notes.where(commit_id: noteable.id)
else
notes = notes.where(noteable_id: noteable.id)
end
gfm_reference = mentioner_gfm_ref(noteable, mentioner, true)
notes = notes.where(note: cross_reference_note_content(gfm_reference))
notes.count > 0
end
private
def self.create_note(args = {})
Note.create(args.merge(system: true))
end
# Prepend the mentioner's namespaced project path to the GFM reference for
# cross-project references. For same-project references, return the
# unmodified GFM reference.
def self.mentioner_gfm_ref(noteable, mentioner, cross_reference = false)
# FIXME (rspeicher): This was breaking things.
# if mentioner.is_a?(Commit) && cross_reference
# return mentioner.gfm_reference.sub('commit ', 'commit %')
# end
full_gfm_reference(mentioner.project, noteable.project, mentioner)
end
# Return the +mentioner+ GFM reference. If the mentioner and noteable
# projects are not the same, add the mentioning project's path to the
# returned value.
def self.full_gfm_reference(mentioning_project, noteable_project, mentioner)
if mentioning_project == noteable_project
mentioner.gfm_reference
else
if mentioner.is_a?(Commit)
mentioner.gfm_reference.sub(
/(commit )/,
"\\1#{mentioning_project.path_with_namespace}@"
)
else
mentioner.gfm_reference.sub(
/(issue |merge request )/,
"\\1#{mentioning_project.path_with_namespace}"
)
end
end
end
def self.cross_reference_note_prefix
'mentioned in '
end
def self.cross_reference_note_content(gfm_reference)
"#{cross_reference_note_prefix}#{gfm_reference}"
end
# Build an Array of lines detailing each commit added in a merge request
#
# new_commits - Array of new Commit objects
#
# Returns an Array of Strings
def self.new_commit_summary(new_commits)
new_commits.collect do |commit|
"* #{commit.short_id} - #{commit.title}"
end
end
# Build a single line summarizing existing commits being added in a merge
# request
#
# noteable - MergeRequest object
# existing_commits - Array of existing Commit objects
# oldrev - Optional String SHA of ... TODO (rspeicher): I have no idea what this actually does.
#
# Examples:
#
# "* ea0f8418...2f4426b7 - 24 commits from branch `master`"
#
# "* ea0f8418..4188f0ea - 15 commits from branch `fork:master`"
#
# "* ea0f8418 - 1 commit from branch `feature`"
#
# Returns a newline-terminated String
def self.existing_commit_summary(noteable, existing_commits, oldrev = nil)
return '' if existing_commits.empty?
count = existing_commits.size
commit_ids = if count == 1
existing_commits.first.short_id
else
if oldrev
"#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
else
"#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
end
end
commits_text = "#{count} commit".pluralize(count)
branch = noteable.target_branch
branch = "#{noteable.target_project_namespace}:#{branch}" if noteable.for_fork?
"* #{commit_ids} - #{commits_text} from branch `#{branch}`\n"
end
end
# Convert legacy Markdown-emphasized notes to the current, non-emphasized format
#
# _mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666_
#
# becomes
#
# mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666
class ConvertLegacyReferenceNotes < ActiveRecord::Migration
def up
execute %q{UPDATE notes SET note = trim(both '_' from note) WHERE system = true AND note LIKE '\_%\_'}
end
def down
# noop
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150502064022) do ActiveRecord::Schema.define(version: 20150509180749) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -31,9 +31,10 @@ FactoryGirl.define do ...@@ -31,9 +31,10 @@ FactoryGirl.define do
factory :note_on_merge_request, traits: [:on_merge_request] factory :note_on_merge_request, traits: [:on_merge_request]
factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff]
factory :note_on_project_snippet, traits: [:on_project_snippet] factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :system_note, traits: [:system]
trait :on_commit do trait :on_commit do
project factory: :project project
commit_id RepoHelpers.sample_commit.id commit_id RepoHelpers.sample_commit.id
noteable_type "Commit" noteable_type "Commit"
end end
...@@ -43,7 +44,7 @@ FactoryGirl.define do ...@@ -43,7 +44,7 @@ FactoryGirl.define do
end end
trait :on_merge_request do trait :on_merge_request do
project factory: :project project
noteable_id 1 noteable_id 1
noteable_type "MergeRequest" noteable_type "MergeRequest"
end end
...@@ -58,6 +59,10 @@ FactoryGirl.define do ...@@ -58,6 +59,10 @@ FactoryGirl.define do
noteable_type "Snippet" noteable_type "Snippet"
end end
trait :system do
system true
end
trait :with_attachment do trait :with_attachment do
attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") }
end end
......
...@@ -20,68 +20,88 @@ ...@@ -20,68 +20,88 @@
require 'spec_helper' require 'spec_helper'
describe Note do describe Note do
describe "Associations" do describe 'associations' do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:noteable) } it { is_expected.to belong_to(:noteable) }
it { is_expected.to belong_to(:author).class_name('User') } it { is_expected.to belong_to(:author).class_name('User') }
end end
describe "Mass assignment" do describe 'validation' do
end
describe "Validation" do
it { is_expected.to validate_presence_of(:note) } it { is_expected.to validate_presence_of(:note) }
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:project) }
end end
describe "Voting score" do describe '#votable?' do
let(:project) { create(:project) } it 'is true for issue notes' do
note = build(:note_on_issue)
expect(note).to be_votable
end
it 'is true for merge request notes' do
note = build(:note_on_merge_request)
expect(note).to be_votable
end
it 'is false for merge request diff notes' do
note = build(:note_on_merge_request_diff)
expect(note).not_to be_votable
end
it 'is false for commit notes' do
note = build(:note_on_commit)
expect(note).not_to be_votable
end
it "recognizes a neutral note" do it 'is false for commit diff notes' do
note = create(:votable_note, note: "This is not a +1 note") note = build(:note_on_commit_diff)
expect(note).not_to be_votable
end
end
describe 'voting score' do
it 'recognizes a neutral note' do
note = build(:votable_note, note: 'This is not a +1 note')
expect(note).not_to be_upvote expect(note).not_to be_upvote
expect(note).not_to be_downvote expect(note).not_to be_downvote
end end
it "recognizes a neutral emoji note" do it 'recognizes a neutral emoji note' do
note = build(:votable_note, note: "I would :+1: this, but I don't want to") note = build(:votable_note, note: "I would :+1: this, but I don't want to")
expect(note).not_to be_upvote expect(note).not_to be_upvote
expect(note).not_to be_downvote expect(note).not_to be_downvote
end end
it "recognizes a +1 note" do it 'recognizes a +1 note' do
note = create(:votable_note, note: "+1 for this") note = build(:votable_note, note: '+1 for this')
expect(note).to be_upvote expect(note).to be_upvote
end end
it "recognizes a +1 emoji as a vote" do it 'recognizes a +1 emoji as a vote' do
note = build(:votable_note, note: ":+1: for this") note = build(:votable_note, note: ':+1: for this')
expect(note).to be_upvote expect(note).to be_upvote
end end
it "recognizes a thumbsup emoji as a vote" do it 'recognizes a thumbsup emoji as a vote' do
note = build(:votable_note, note: ":thumbsup: for this") note = build(:votable_note, note: ':thumbsup: for this')
expect(note).to be_upvote expect(note).to be_upvote
end end
it "recognizes a -1 note" do it 'recognizes a -1 note' do
note = create(:votable_note, note: "-1 for this") note = build(:votable_note, note: '-1 for this')
expect(note).to be_downvote expect(note).to be_downvote
end end
it "recognizes a -1 emoji as a vote" do it 'recognizes a -1 emoji as a vote' do
note = build(:votable_note, note: ":-1: for this") note = build(:votable_note, note: ':-1: for this')
expect(note).to be_downvote expect(note).to be_downvote
end end
it "recognizes a thumbsdown emoji as a vote" do it 'recognizes a thumbsdown emoji as a vote' do
note = build(:votable_note, note: ":thumbsdown: for this") note = build(:votable_note, note: ':thumbsdown: for this')
expect(note).to be_downvote expect(note).to be_downvote
end end
end end
let(:project) { create(:project) }
describe "Commit notes" do describe "Commit notes" do
let!(:note) { create(:note_on_commit, note: "+1 from me") } let!(:note) { create(:note_on_commit, note: "+1 from me") }
let!(:commit) { note.noteable } let!(:commit) { note.noteable }
...@@ -100,10 +120,6 @@ describe Note do ...@@ -100,10 +120,6 @@ describe Note do
it "should be recognized by #for_commit?" do it "should be recognized by #for_commit?" do
expect(note).to be_for_commit expect(note).to be_for_commit
end end
it "should not be votable" do
expect(note).not_to be_votable
end
end end
describe "Commit diff line notes" do describe "Commit diff line notes" do
...@@ -128,461 +144,7 @@ describe Note do ...@@ -128,461 +144,7 @@ describe Note do
end end
end end
describe "Issue notes" do describe 'authorization' do
let!(:note) { create(:note_on_issue, note: "+1 from me") }
it "should not be votable" do
expect(note).to be_votable
end
end
describe "Merge request notes" do
let!(:note) { create(:note_on_merge_request, note: "+1 from me") }
it "should be votable" do
expect(note).to be_votable
end
end
describe "Merge request diff line notes" do
let!(:note) { create(:note_on_merge_request_diff, note: "+1 from me") }
it "should not be votable" do
expect(note).not_to be_votable
end
end
describe '#create_status_change_note' do
let(:project) { create(:project) }
let(:thing) { create(:issue, project: project) }
let(:author) { create(:user) }
let(:status) { 'new_status' }
subject { Note.create_status_change_note(thing, project, author, status, nil) }
it 'creates and saves a Note' do
is_expected.to be_a Note
expect(subject.id).not_to be_nil
end
describe '#noteable' do
subject { super().noteable }
it { is_expected.to eq(thing) }
end
describe '#project' do
subject { super().project }
it { is_expected.to eq(thing.project) }
end
describe '#author' do
subject { super().author }
it { is_expected.to eq(author) }
end
describe '#note' do
subject { super().note }
it { is_expected.to eq("Status changed to #{status}") }
end
it 'appends a back-reference if a closing mentionable is supplied' do
commit = double('commit', gfm_reference: 'commit 123456')
n = Note.create_status_change_note(thing, project, author, status, commit)
expect(n.note).to eq("Status changed to #{status} by commit 123456")
end
end
describe '#create_assignee_change_note' do
let(:project) { create(:project) }
let(:thing) { create(:issue, project: project) }
let(:author) { create(:user) }
let(:assignee) { create(:user, username: "assigned_user") }
subject { Note.create_assignee_change_note(thing, project, author, assignee) }
context 'creates and saves a Note' do
it { is_expected.to be_a Note }
describe '#id' do
subject { super().id }
it { is_expected.not_to be_nil }
end
end
describe '#noteable' do
subject { super().noteable }
it { is_expected.to eq(thing) }
end
describe '#project' do
subject { super().project }
it { is_expected.to eq(thing.project) }
end
describe '#author' do
subject { super().author }
it { is_expected.to eq(author) }
end
describe '#note' do
subject { super().note }
it { is_expected.to eq('Reassigned to @assigned_user') }
end
context 'assignee is removed' do
let(:assignee) { nil }
describe '#note' do
subject { super().note }
it { is_expected.to eq('Assignee removed') }
end
end
end
describe '#create_labels_change_note' do
let(:project) { create(:project) }
let(:thing) { create(:issue, project: project) }
let(:author) { create(:user) }
let(:label1) { create(:label) }
let(:label2) { create(:label) }
let(:added_labels) { [label1, label2] }
let(:removed_labels) { [] }
subject { Note.create_labels_change_note(thing, project, author, added_labels, removed_labels) }
context 'creates and saves a Note' do
it { is_expected.to be_a Note }
describe '#id' do
subject { super().id }
it { is_expected.not_to be_nil }
end
end
describe '#noteable' do
subject { super().noteable }
it { is_expected.to eq(thing) }
end
describe '#project' do
subject { super().project }
it { is_expected.to eq(thing.project) }
end
describe '#author' do
subject { super().author }
it { is_expected.to eq(author) }
end
describe '#note' do
subject { super().note }
it { is_expected.to eq("Added ~#{label1.id} ~#{label2.id} labels") }
end
context 'label is removed' do
let(:added_labels) { [label1] }
let(:removed_labels) { [label2] }
describe '#note' do
subject { super().note }
it { is_expected.to eq("Added ~#{label1.id} and removed ~#{label2.id} labels") }
end
end
end
describe '#create_milestone_change_note' do
let(:project) { create(:project) }
let(:thing) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project, title: "first_milestone") }
let(:author) { create(:user) }
subject { Note.create_milestone_change_note(thing, project, author, milestone) }
context 'creates and saves a Note' do
it { is_expected.to be_a Note }
describe '#id' do
subject { super().id }
it { is_expected.not_to be_nil }
end
end
describe '#project' do
subject { super().project }
it { is_expected.to eq(thing.project) }
end
describe '#author' do
subject { super().author }
it { is_expected.to eq(author) }
end
describe '#note' do
subject { super().note }
it { is_expected.to eq("Milestone changed to first_milestone") }
end
end
describe '#create_cross_reference_note' do
let(:project) { create(:project) }
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
let(:commit) { project.commit }
# Test all of {issue, merge request, commit} in both the referenced and referencing
# roles, to ensure that the correct information can be inferred from any argument.
context 'issue from a merge request' do
subject { Note.create_cross_reference_note(issue, mergereq, author) }
it { is_expected.to be_valid }
describe '#noteable' do
subject { super().noteable }
it { is_expected.to eq(issue) }
end
describe '#project' do
subject { super().project }
it { is_expected.to eq(issue.project) }
end
describe '#author' do
subject { super().author }
it { is_expected.to eq(author) }
end
describe '#note' do
subject { super().note }
it { is_expected.to eq("mentioned in merge request !#{mergereq.iid}") }
end
end
context 'issue from a commit' do
subject { Note.create_cross_reference_note(issue, commit, author) }
it { is_expected.to be_valid }
describe '#noteable' do
subject { super().noteable }
it { is_expected.to eq(issue) }
end
describe '#note' do
subject { super().note }
it { is_expected.to eq("mentioned in commit #{commit.sha}") }
end
end
context 'merge request from an issue' do
subject { Note.create_cross_reference_note(mergereq, issue, author) }
it { is_expected.to be_valid }
describe '#noteable' do
subject { super().noteable }
it { is_expected.to eq(mergereq) }
end
describe '#project' do
subject { super().project }
it { is_expected.to eq(mergereq.project) }
end
describe '#note' do
subject { super().note }
it { is_expected.to eq("mentioned in issue ##{issue.iid}") }
end
end
context 'commit from a merge request' do
subject { Note.create_cross_reference_note(commit, mergereq, author) }
it { is_expected.to be_valid }
describe '#noteable' do
subject { super().noteable }
it { is_expected.to eq(commit) }
end
describe '#project' do
subject { super().project }
it { is_expected.to eq(project) }
end
describe '#note' do
subject { super().note }
it { is_expected.to eq("mentioned in merge request !#{mergereq.iid}") }
end
end
context 'commit contained in a merge request' do
subject { Note.create_cross_reference_note(mergereq.commits.first, mergereq, author) }
it { is_expected.to be_nil }
end
context 'commit from issue' do
subject { Note.create_cross_reference_note(commit, issue, author) }
it { is_expected.to be_valid }
describe '#noteable_type' do
subject { super().noteable_type }
it { is_expected.to eq("Commit") }
end
describe '#noteable_id' do
subject { super().noteable_id }
it { is_expected.to be_nil }
end
describe '#commit_id' do
subject { super().commit_id }
it { is_expected.to eq(commit.id) }
end
describe '#note' do
subject { super().note }
it { is_expected.to eq("mentioned in issue ##{issue.iid}") }
end
end
context 'commit from commit' do
let(:parent_commit) { commit.parents.first }
subject { Note.create_cross_reference_note(commit, parent_commit, author) }
it { is_expected.to be_valid }
describe '#noteable_type' do
subject { super().noteable_type }
it { is_expected.to eq("Commit") }
end
describe '#noteable_id' do
subject { super().noteable_id }
it { is_expected.to be_nil }
end
describe '#commit_id' do
subject { super().commit_id }
it { is_expected.to eq(commit.id) }
end
describe '#note' do
subject { super().note }
it { is_expected.to eq("mentioned in commit #{parent_commit.id}") }
end
end
end
describe '#cross_reference_exists?' do
let(:project) { create :project }
let(:author) { create :user }
let(:issue) { create :issue }
let(:commit0) { project.commit }
let(:commit1) { project.commit('HEAD~2') }
before do
Note.create_cross_reference_note(issue, commit0, author)
end
it 'detects if a mentionable has already been mentioned' do
expect(Note.cross_reference_exists?(issue, commit0)).to be_truthy
end
it 'detects if a mentionable has not already been mentioned' do
expect(Note.cross_reference_exists?(issue, commit1)).to be_falsey
end
context 'commit on commit' do
before do
Note.create_cross_reference_note(commit0, commit1, author)
end
it { expect(Note.cross_reference_exists?(commit0, commit1)).to be_truthy }
it { expect(Note.cross_reference_exists?(commit1, commit0)).to be_falsey }
end
context 'legacy note with Markdown emphasis' do
let(:issue2) { create :issue, project: project }
let!(:note) do
create :note, system: true, noteable_id: issue2.id,
noteable_type: "Issue", note: "_mentioned in issue " \
"#{issue.project.path_with_namespace}##{issue.iid}_"
end
it 'detects if a mentionable with emphasis has been mentioned' do
expect(Note.cross_reference_exists?(issue2, issue)).to be_truthy
end
end
end
describe '#cross_references_with_underscores?' do
let(:project) { create :project, path: "first_project" }
let(:second_project) { create :project, path: "second_project" }
let(:author) { create :user }
let(:issue0) { create :issue, project: project }
let(:issue1) { create :issue, project: second_project }
let!(:note) { Note.create_cross_reference_note(issue0, issue1, author) }
it 'detects if a mentionable has already been mentioned' do
expect(Note.cross_reference_exists?(issue0, issue1)).to be_truthy
end
it 'detects if a mentionable has not already been mentioned' do
expect(Note.cross_reference_exists?(issue1, issue0)).to be_falsey
end
it 'detects that text has underscores' do
expect(note.note).to eq("mentioned in issue #{second_project.path_with_namespace}##{issue1.iid}")
end
end
describe '#system?' do
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
let(:other) { create(:issue, project: project) }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
let(:label) { create(:label) }
let(:milestone) { create(:milestone) }
it 'should recognize user-supplied notes as non-system' do
@note = create(:note_on_issue)
expect(@note).not_to be_system
end
it 'should identify status-change notes as system notes' do
@note = Note.create_status_change_note(issue, project, author, 'closed', nil)
expect(@note).to be_system
end
it 'should identify cross-reference notes as system notes' do
@note = Note.create_cross_reference_note(issue, other, author)
expect(@note).to be_system
end
it 'should identify assignee-change notes as system notes' do
@note = Note.create_assignee_change_note(issue, project, author, assignee)
expect(@note).to be_system
end
it 'should identify label-change notes as system notes' do
@note = Note.create_labels_change_note(issue, project, author, [label], [])
expect(@note).to be_system
end
it 'should identify milestone-change notes as system notes' do
@note = Note.create_milestone_change_note(issue, project, author, milestone)
expect(@note).to be_system
end
end
describe :authorization do
before do before do
@p1 = create(:project) @p1 = create(:project)
@p2 = create(:project) @p2 = create(:project)
...@@ -593,7 +155,7 @@ describe Note do ...@@ -593,7 +155,7 @@ describe Note do
@abilities << Ability @abilities << Ability
end end
describe :read do describe 'read' do
before do before do
@p1.project_members.create(user: @u2, access_level: ProjectMember::GUEST) @p1.project_members.create(user: @u2, access_level: ProjectMember::GUEST)
@p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST) @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST)
...@@ -604,7 +166,7 @@ describe Note do ...@@ -604,7 +166,7 @@ describe Note do
it { expect(@abilities.allowed?(@u3, :read_note, @p1)).to be_falsey } it { expect(@abilities.allowed?(@u3, :read_note, @p1)).to be_falsey }
end end
describe :write do describe 'write' do
before do before do
@p1.project_members.create(user: @u2, access_level: ProjectMember::DEVELOPER) @p1.project_members.create(user: @u2, access_level: ProjectMember::DEVELOPER)
@p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER) @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER)
...@@ -615,7 +177,7 @@ describe Note do ...@@ -615,7 +177,7 @@ describe Note do
it { expect(@abilities.allowed?(@u3, :write_note, @p1)).to be_falsey } it { expect(@abilities.allowed?(@u3, :write_note, @p1)).to be_falsey }
end end
describe :admin do describe 'admin' do
before do before do
@p1.project_members.create(user: @u1, access_level: ProjectMember::REPORTER) @p1.project_members.create(user: @u1, access_level: ProjectMember::REPORTER)
@p1.project_members.create(user: @u2, access_level: ProjectMember::MASTER) @p1.project_members.create(user: @u2, access_level: ProjectMember::MASTER)
...@@ -631,6 +193,7 @@ describe Note do ...@@ -631,6 +193,7 @@ describe Note do
it_behaves_like 'an editable mentionable' do it_behaves_like 'an editable mentionable' do
subject { create :note, noteable: issue, project: project } subject { create :note, noteable: issue, project: project }
let(:project) { create(:project) }
let(:issue) { create :issue, project: project } let(:issue) { create :issue, project: project }
let(:backref_text) { issue.gfm_reference } let(:backref_text) { issue.gfm_reference }
let(:set_mentionable_text) { ->(txt) { subject.note = txt } } let(:set_mentionable_text) { ->(txt) { subject.note = txt } }
......
require 'spec_helper'
describe SystemNoteService do
let(:project) { create(:project) }
let(:author) { create(:user) }
let(:noteable) { create(:issue, project: project) }
shared_examples_for 'a system note' do
it 'is valid' do
expect(subject).to be_valid
end
it 'sets the noteable model' do
expect(subject.noteable).to eq noteable
end
it 'sets the project' do
expect(subject.project).to eq project
end
it 'sets the author' do
expect(subject.author).to eq author
end
it 'is a system note' do
expect(subject).to be_system
end
end
describe '.add_commits' do
subject { described_class.add_commits(noteable, project, author, new_commits, old_commits, oldrev) }
let(:noteable) { create(:merge_request, source_project: project) }
let(:new_commits) { noteable.commits }
let(:old_commits) { [] }
let(:oldrev) { nil }
it_behaves_like 'a system note'
describe 'note body' do
let(:note_lines) { subject.note.split("\n").reject(&:blank?) }
context 'without existing commits' do
it 'adds a message header' do
expect(note_lines[0]).to eq "Added #{new_commits.size} commits:"
end
it 'adds a message line for each commit' do
new_commits.each_with_index do |commit, i|
# Skip the header
expect(note_lines[i + 1]).to eq "* #{commit.short_id} - #{commit.title}"
end
end
end
describe 'summary line for existing commits' do
let(:summary_line) { note_lines[1] }
context 'with one existing commit' do
let(:old_commits) { [noteable.commits.last] }
it 'includes the existing commit' do
expect(summary_line).to eq "* #{old_commits.first.short_id} - 1 commit from branch `feature`"
end
end
context 'with multiple existing commits' do
let(:old_commits) { noteable.commits[3..-1] }
context 'with oldrev' do
let(:oldrev) { noteable.commits[2].id }
it 'includes a commit range' do
expect(summary_line).to start_with "* #{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id}"
end
it 'includes a commit count' do
expect(summary_line).to end_with " - 2 commits from branch `feature`"
end
end
context 'without oldrev' do
it 'includes a commit range' do
expect(summary_line).to start_with "* #{old_commits[0].short_id}..#{old_commits[-1].short_id}"
end
it 'includes a commit count' do
expect(summary_line).to end_with " - 2 commits from branch `feature`"
end
end
context 'on a fork' do
before do
expect(noteable).to receive(:for_fork?).and_return(true)
end
it 'includes the project namespace' do
expect(summary_line).to end_with "`#{noteable.target_project_namespace}:feature`"
end
end
end
end
end
end
describe '.change_assignee' do
subject { described_class.change_assignee(noteable, project, author, assignee) }
let(:assignee) { create(:user) }
it_behaves_like 'a system note'
context 'when assignee added' do
it 'sets the note text' do
expect(subject.note).to eq "Reassigned to @#{assignee.username}"
end
end
context 'when assignee removed' do
let(:assignee) { nil }
it 'sets the note text' do
expect(subject.note).to eq 'Assignee removed'
end
end
end
describe '.change_label' do
subject { described_class.change_label(noteable, project, author, added, removed) }
let(:labels) { create_list(:label, 2) }
let(:added) { [] }
let(:removed) { [] }
it_behaves_like 'a system note'
context 'with added labels' do
let(:added) { labels }
let(:removed) { [] }
it 'sets the note text' do
expect(subject.note).to eq "Added ~#{labels[0].id} ~#{labels[1].id} labels"
end
end
context 'with removed labels' do
let(:added) { [] }
let(:removed) { labels }
it 'sets the note text' do
expect(subject.note).to eq "Removed ~#{labels[0].id} ~#{labels[1].id} labels"
end
end
context 'with added and removed labels' do
let(:added) { [labels[0]] }
let(:removed) { [labels[1]] }
it 'sets the note text' do
expect(subject.note).to eq "Added ~#{labels[0].id} and removed ~#{labels[1].id} labels"
end
end
end
describe '.change_milestone' do
subject { described_class.change_milestone(noteable, project, author, milestone) }
let(:milestone) { create(:milestone, project: project) }
it_behaves_like 'a system note'
context 'when milestone added' do
it 'sets the note text' do
expect(subject.note).to eq "Milestone changed to #{milestone.title}"
end
end
context 'when milestone removed' do
let(:milestone) { nil }
it 'sets the note text' do
expect(subject.note).to eq 'Milestone removed'
end
end
end
describe '.change_status' do
subject { described_class.change_status(noteable, project, author, status, source) }
let(:status) { 'new_status' }
let(:source) { nil }
it_behaves_like 'a system note'
context 'with a source' do
let(:source) { double('commit', gfm_reference: 'commit 123456') }
it 'sets the note text' do
expect(subject.note).to eq "Status changed to #{status} by commit 123456"
end
end
context 'without a source' do
it 'sets the note text' do
expect(subject.note).to eq "Status changed to #{status}"
end
end
end
describe '.cross_reference' do
subject { described_class.cross_reference(noteable, mentioner, author) }
let(:mentioner) { create(:issue, project: project) }
it_behaves_like 'a system note'
context 'when cross-reference disallowed' do
before do
expect(described_class).to receive(:cross_reference_disallowed?).and_return(true)
end
it 'returns nil' do
expect(subject).to be_nil
end
end
context 'when cross-reference allowed' do
before do
expect(described_class).to receive(:cross_reference_disallowed?).and_return(false)
end
describe 'note_body' do
context 'cross-project' do
let(:project2) { create(:project) }
let(:mentioner) { create(:issue, project: project2) }
context 'from Commit' do
let(:mentioner) { project2.repository.commit }
it 'references the mentioning commit' do
expect(subject.note).to eq "mentioned in commit #{project2.path_with_namespace}@#{mentioner.id}"
end
end
context 'from non-Commit' do
it 'references the mentioning object' do
expect(subject.note).to eq "mentioned in issue #{project2.path_with_namespace}##{mentioner.iid}"
end
end
end
context 'within the same project' do
context 'from Commit' do
let(:mentioner) { project.repository.commit }
it 'references the mentioning commit' do
expect(subject.note).to eq "mentioned in commit #{mentioner.id}"
end
end
context 'from non-Commit' do
it 'references the mentioning object' do
expect(subject.note).to eq "mentioned in issue ##{mentioner.iid}"
end
end
end
end
end
end
describe '.cross_reference?' do
it 'is truthy when text begins with expected text' do
expect(described_class.cross_reference?('mentioned in issue #1')).to be_truthy
end
it 'is falsey when text does not begin with expected text' do
expect(described_class.cross_reference?('this is a note')).to be_falsey
end
end
describe '.cross_reference_disallowed?' do
context 'when mentioner is not a MergeRequest' do
it 'is falsey' do
mentioner = noteable.dup
expect(described_class.cross_reference_disallowed?(noteable, mentioner)).
to be_falsey
end
end
context 'when mentioner is a MergeRequest' do
let(:mentioner) { create(:merge_request, :simple, source_project: project) }
let(:noteable) { project.commit }
it 'is truthy when noteable is in commits' do
expect(mentioner).to receive(:commits).and_return([noteable])
expect(described_class.cross_reference_disallowed?(noteable, mentioner)).
to be_truthy
end
it 'is falsey when noteable is not in commits' do
expect(mentioner).to receive(:commits).and_return([])
expect(described_class.cross_reference_disallowed?(noteable, mentioner)).
to be_falsey
end
end
end
describe '.cross_reference_exists?' do
let(:commit0) { project.commit }
let(:commit1) { project.commit('HEAD~2') }
context 'issue from commit' do
before do
# Mention issue (noteable) from commit0
described_class.cross_reference(noteable, commit0, author)
end
it 'is truthy when already mentioned' do
expect(described_class.cross_reference_exists?(noteable, commit0)).
to be_truthy
end
it 'is falsey when not already mentioned' do
expect(described_class.cross_reference_exists?(noteable, commit1)).
to be_falsey
end
end
context 'commit from commit' do
before do
# Mention commit1 from commit0
described_class.cross_reference(commit0, commit1, author)
end
it 'is truthy when already mentioned' do
expect(described_class.cross_reference_exists?(commit0, commit1)).
to be_truthy
end
it 'is falsey when not already mentioned' do
expect(described_class.cross_reference_exists?(commit1, commit0)).
to be_falsey
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