summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Derichs <pderichs@gitlab.com>2019-09-06 10:31:12 +0200
committerPatrick Derichs <pderichs@gitlab.com>2019-09-12 10:22:57 +0200
commit8b57b78db7ab7c35bf62c76a83ef00e25c40edbb (patch)
tree4f1cd758dea6e286a06da5f50903ae123df0eb79
parent31b76c6ee1a89ad2ecf3c55aa9635df887fae0f3 (diff)
downloadgitlab-ce-30000-split-up-system-note-service.tar.gz
Split up SystemNoteService30000-split-up-system-note-service
Extracted topic related services but kept SystemNoteService in place for now.
-rw-r--r--app/models/note.rb2
-rw-r--r--app/services/system_note_service.rb728
-rw-r--r--app/services/system_notes/base_system_note_service.rb26
-rw-r--r--app/services/system_notes/commit_system_notes_service.rb117
-rw-r--r--app/services/system_notes/issuables_system_note_service.rb329
-rw-r--r--app/services/system_notes/merge_requests_system_note_service.rb143
-rw-r--r--app/services/system_notes/time_tracking_system_note_service.rb80
-rw-r--r--app/services/system_notes/zoom_system_note_service.rb11
-rw-r--r--spec/services/system_note_service_spec.rb4
9 files changed, 811 insertions, 629 deletions
diff --git a/app/models/note.rb b/app/models/note.rb
index 62b3f47fadd..0faaf5aa490 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -215,7 +215,7 @@ class Note < ApplicationRecord
if force_cross_reference_regex_check?
matches_cross_reference_regex?
else
- SystemNoteService.cross_reference?(note)
+ IssuablesSystemNoteService.cross_reference?(note)
end
end
# rubocop: enable CodeReuse/ServiceClass
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 1b48b20e28b..9e565ca98ee 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -7,702 +7,178 @@
module SystemNoteService
extend self
- # 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 - Optional String SHA of a previous Commit
- #
- # See new_commit_summary and existing_commit_summary.
- #
- # Returns the created Note object
def 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)
-
- text_parts = ["added #{commits_text}"]
- text_parts << commits_list(noteable, new_commits, existing_commits, oldrev)
- text_parts << "[Compare with previous version](#{diff_comparison_path(noteable, project, oldrev)})"
-
- body = text_parts.join("\n\n")
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count))
+ service = CommitSystemNoteService.new(noteable, project, author)
+ service.add_commits(new_commits, existing_commits, oldrev)
end
- # Called when a commit was tagged
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the tag
- # tag_name - The created tag name
- #
- # Returns the created Note object
def tag_commit(noteable, project, author, tag_name)
- link = url_helpers.project_tag_path(project, id: tag_name)
- body = "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'tag'))
- 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:
- #
- # "removed assignee"
- #
- # "assigned to @rspeicher"
- #
- # Returns the created Note object
+ service = CommitSystemNoteService.new(noteable, project, author)
+ service.tag_commit(tag_name)
+ end
+
def change_assignee(noteable, project, author, assignee)
- body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
- end
-
- # Called when the assignees of an issuable is changed or removed
- #
- # issuable - Issuable object (responds to assignees)
- # project - Project owning noteable
- # author - User performing the change
- # assignees - Users being assigned, or nil
- #
- # Example Note text:
- #
- # "removed all assignees"
- #
- # "assigned to @user1 additionally to @user2"
- #
- # "assigned to @user1, @user2 and @user3 and unassigned from @user4 and @user5"
- #
- # "assigned to @user1 and @user2"
- #
- # Returns the created Note object
+ service = IssuablesSystemNoteService.new(noteable, project, author)
+ service.change_assignee(assignee)
+ end
+
def change_issuable_assignees(issuable, project, author, old_assignees)
- unassigned_users = old_assignees - issuable.assignees
- added_users = issuable.assignees.to_a - old_assignees
- text_parts = []
-
- Gitlab::I18n.with_default_locale do
- text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
- text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
- end
-
- body = text_parts.join(' and ')
-
- create_note(NoteSummary.new(issuable, project, author, body, action: 'assignee'))
- 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:
- #
- # "removed milestone"
- #
- # "changed milestone to 7.11"
- #
- # Returns the created Note object
+ service = IssuablesSystemNoteService.new(issuable, project, author)
+ service.change_issuable_assignees(old_assignees)
+ end
+
def change_milestone(noteable, project, author, milestone)
- format = milestone&.group_milestone? ? :name : :iid
- body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
- end
-
- # Called when the due_date of a Noteable is changed
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # due_date - Due date being assigned, or nil
- #
- # Example Note text:
- #
- # "removed due date"
- #
- # "changed due date to September 20, 2018"
- #
- # Returns the created Note object
+ service = IssuablesSystemNoteService.new(noteable, project, author)
+ service.change_milestone(milestone)
+ end
+
def change_due_date(noteable, project, author, due_date)
- body = due_date ? "changed due date to #{due_date.to_s(:long)}" : 'removed due date'
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'due_date'))
- end
-
- # Called when the estimated time of a Noteable is changed
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # time_estimate - Estimated time
- #
- # Example Note text:
- #
- # "removed time estimate"
- #
- # "changed time estimate to 3d 5h"
- #
- # Returns the created Note object
+ service = TimeTrackingSystemNoteService.new(noteable, project, author)
+ service.change_due_date(due_date)
+ end
+
def change_time_estimate(noteable, project, author)
- parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
- body = if noteable.time_estimate == 0
- "removed time estimate"
- else
- "changed time estimate to #{parsed_time}"
- end
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
- end
-
- # Called when the spent time of a Noteable is changed
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # time_spent - Spent time
- #
- # Example Note text:
- #
- # "removed time spent"
- #
- # "added 2h 30m of time spent"
- #
- # Returns the created Note object
- def change_time_spent(noteable, project, author)
- time_spent = noteable.time_spent
-
- if time_spent == :reset
- body = "removed time spent"
- else
- spent_at = noteable.spent_at
- parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
- action = time_spent > 0 ? 'added' : 'subtracted'
-
- text_parts = ["#{action} #{parsed_time} of time spent"]
- text_parts << "at #{spent_at}" if spent_at
- body = text_parts.join(' ')
- end
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
- 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:
- #
- # "merged"
- #
- # "closed via bc17db76"
- #
- # Returns the created Note object
- def change_status(noteable, project, author, status, source = nil)
- body = status.dup
- body << " via #{source.gfm_reference(project)}" if source
+ service = TimeTrackingSystemNoteService.new(noteable, project, author)
+ service.change_time_estimate
+ end
- action = status == 'reopened' ? 'opened' : status
+ def change_time_spent(noteable, project, author)
+ service = TimeTrackingSystemNoteService.new(noteable, project, author)
+ service.change_time_spent
+ end
- create_note(NoteSummary.new(noteable, project, author, body, action: action))
+ def change_status(noteable, project, author, status, source = nil)
+ service = IssuablesSystemNoteService.new(noteable, project, author)
+ service.change_status(status, source)
end
- # Called when 'merge when pipeline succeeds' is executed
def merge_when_pipeline_succeeds(noteable, project, author, sha)
- body = "enabled an automatic merge when the pipeline for #{sha} succeeds"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ service = MergeRequestsSystemNoteService.new(noteable, project, author)
+ service.merge_when_pipeline_succeeds(sha)
end
- # Called when 'merge when pipeline succeeds' is canceled
def cancel_merge_when_pipeline_succeeds(noteable, project, author)
- body = 'canceled the automatic merge'
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ service = MergeRequestsSystemNoteService.new(noteable, project, author)
+ service.cancel_merge_when_pipeline_succeeds
end
- # Called when 'merge when pipeline succeeds' is aborted
def abort_merge_when_pipeline_succeeds(noteable, project, author, reason)
- body = "aborted the automatic merge because #{reason}"
-
- ##
- # TODO: Abort message should be sent by the system, not a particular user.
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63187.
- create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ service = MergeRequestsSystemNoteService.new(noteable, project, author)
+ service.abort_merge_when_pipeline_succeeds(reason)
end
def handle_merge_request_wip(noteable, project, author)
- prefix = noteable.work_in_progress? ? "marked" : "unmarked"
-
- body = "#{prefix} as a **Work In Progress**"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ service = MergeRequestsSystemNoteService.new(noteable, project, author)
+ service.handle_merge_request_wip
end
def add_merge_request_wip_from_commit(noteable, project, author, commit)
- body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ service = MergeRequestsSystemNoteService.new(noteable, project, author)
+ service.add_merge_request_wip_from_commit(commit)
end
def resolve_all_discussions(merge_request, project, author)
- body = "resolved all threads"
-
- create_note(NoteSummary.new(merge_request, project, author, body, action: 'discussion'))
+ service = MergeRequestsSystemNoteService.new(merge_request, project, author)
+ service.resolve_all_discussions
end
def discussion_continued_in_issue(discussion, project, author, issue)
- body = "created #{issue.to_reference} to continue this discussion"
- note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
-
- note = Note.create(note_attributes.merge(system: true, created_at: issue.system_note_timestamp))
- note.system_note_metadata = SystemNoteMetadata.new(action: 'discussion')
-
- note
+ service = MergeRequestsSystemNoteService.new(nil, project, author)
+ service.discussion_continued_in_issue(discussion, issue)
end
def diff_discussion_outdated(discussion, project, author, change_position)
- merge_request = discussion.noteable
- diff_refs = change_position.diff_refs
- version_index = merge_request.merge_request_diffs.viewable.count
- position_on_text = change_position.on_text?
- text_parts = ["changed this #{position_on_text ? 'line' : 'file'} in"]
-
- if version_params = merge_request.version_params_for(diff_refs)
- repository = project.repository
- anchor = position_on_text ? change_position.line_code(repository) : change_position.file_hash
- url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: anchor))
-
- text_parts << "[version #{version_index} of the diff](#{url})"
- else
- text_parts << "version #{version_index} of the diff"
- end
-
- body = text_parts.join(' ')
- note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
-
- note = Note.create(note_attributes.merge(system: true))
- note.system_note_metadata = SystemNoteMetadata.new(action: 'outdated')
-
- note
- end
-
- # Called when the title of a Noteable is changed
- #
- # noteable - Noteable object that responds to `title`
- # project - Project owning noteable
- # author - User performing the change
- # old_title - Previous String title
- #
- # Example Note text:
- #
- # "changed title from **Old** to **New**"
- #
- # Returns the created Note object
- def change_title(noteable, project, author, old_title)
- new_title = noteable.title.dup
-
- old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs
-
- marked_old_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(old_title).mark(old_diffs, mode: :deletion)
- marked_new_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(new_title).mark(new_diffs, mode: :addition)
-
- body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"
+ service = MergeRequestsSystemNoteService.new(nil, project, author)
+ service.diff_discussion_outdated(discussion, change_position)
+ end
- create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ def change_title(noteable, project, author, old_title)
+ service = IssuablesSystemNoteService.new(noteable, project, author)
+ service.change_title(old_title)
end
- # Called when the description of a Noteable is changed
- #
- # noteable - Noteable object that responds to `description`
- # project - Project owning noteable
- # author - User performing the change
- #
- # Example Note text:
- #
- # "changed the description"
- #
- # Returns the created Note object
def change_description(noteable, project, author)
- body = 'changed the description'
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'description'))
- end
-
- # Called when the confidentiality changes
- #
- # issue - Issue object
- # project - Project owning the issue
- # author - User performing the change
- #
- # Example Note text:
- #
- # "made the issue confidential"
- #
- # Returns the created Note object
- def change_issue_confidentiality(issue, project, author)
- if issue.confidential
- body = 'made the issue confidential'
- action = 'confidential'
- else
- body = 'made the issue visible to everyone'
- action = 'visible'
- end
-
- create_note(NoteSummary.new(issue, project, author, body, action: action))
- end
-
- # Called when a branch in Noteable is changed
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # branch_type - 'source' or 'target'
- # old_branch - old branch name
- # new_branch - new branch name
- #
- # Example Note text:
- #
- # "changed target branch from `Old` to `New`"
- #
- # Returns the created Note object
- def change_branch(noteable, project, author, branch_type, old_branch, new_branch)
- body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
- end
-
- # Called when a branch in Noteable is added or deleted
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # branch_type - :source or :target
- # branch - branch name
- # presence - :add or :delete
- #
- # Example Note text:
- #
- # "restored target branch `feature`"
- #
- # Returns the created Note object
- def change_branch_presence(noteable, project, author, branch_type, branch, presence)
- verb =
- if presence == :add
- 'restored'
- else
- 'deleted'
- end
-
- body = "#{verb} #{branch_type} branch `#{branch}`"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
+ service = IssuablesSystemNoteService.new(noteable, project, author)
+ service.change_description
end
- # Called when a branch is created from the 'new branch' button on a issue
- # Example note text:
- #
- # "created branch `201-issue-branch-button`"
- def new_issue_branch(issue, project, author, branch, branch_project: nil)
- branch_project ||= project
- link = url_helpers.project_compare_path(branch_project, from: branch_project.default_branch, to: branch)
-
- body = "created branch [`#{branch}`](#{link}) to address this issue"
-
- create_note(NoteSummary.new(issue, project, author, body, action: 'branch'))
+ def change_issue_confidentiality(issue, project, author)
+ service = IssuablesSystemNoteService.new(issue, project, author)
+ service.change_issue_confidentiality
end
- def new_merge_request(issue, project, author, merge_request)
- body = "created merge request #{merge_request.to_reference(project)} to address this issue"
-
- create_note(NoteSummary.new(issue, project, author, body, action: 'merge'))
- 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 cross_reference(noteable, mentioner, author)
- return if cross_reference_disallowed?(noteable, mentioner)
-
- gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
- body = cross_reference_note_content(gfm_reference)
-
- if noteable.is_a?(ExternalIssue)
- noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author)
- else
- create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))
- end
- 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. Additionally, it prevents the creation of references to
- # external issues (which would fail).
- #
- # noteable - Noteable object being referenced
- # mentioner - Mentionable object
- #
- # Returns Boolean
- def cross_reference_disallowed?(noteable, mentioner)
- return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
- return false unless mentioner.is_a?(MergeRequest)
- return false unless noteable.is_a?(Commit)
-
- mentioner.commits.include?(noteable)
- end
-
- # Check if a cross reference to a noteable from a mentioner already exists
- #
- # This method is used to prevent multiple notes being created for a mention
- # when a issue is updated, for example. The method also calls notes_for_mentioner
- # to check if the mentioner is a commit, and return matches only on commit hash
- # instead of project + commit, to avoid repeated mentions from forks.
- #
- # noteable - Noteable object being referenced
- # mentioner - Mentionable object
- #
- # Returns Boolean
- def cross_reference_exists?(noteable, mentioner)
- notes = noteable.notes.system
- notes_for_mentioner(mentioner, noteable, notes).exists?
+ def change_branch(noteable, project, author, branch_type, old_branch, new_branch)
+ service = MergeRequestsSystemNoteService.new(noteable, project, author)
+ service.change_branch(branch_type, old_branch, new_branch)
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 new_commit_summary(new_commits)
- new_commits.collect do |commit|
- content_tag('li', "#{commit.short_id} - #{commit.title}")
- end
- end
-
- # Called when the status of a Task has changed
- #
- # noteable - Noteable object.
- # project - Project owning noteable
- # author - User performing the change
- # new_task - TaskList::Item object.
- #
- # Example Note text:
- #
- # "marked the task Whatever as completed."
- #
- # Returns the created Note object
- def change_task_status(noteable, project, author, new_task)
- status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
- body = "marked the task **#{new_task.source}** as #{status_label}"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'task'))
- end
-
- # Called when noteable has been moved to another project
- #
- # direction - symbol, :to or :from
- # noteable - Noteable object
- # noteable_ref - Referenced noteable
- # author - User performing the move
- #
- # Example Note text:
- #
- # "moved to some_namespace/project_new#11"
- #
- # Returns the created Note object
- def noteable_moved(noteable, project, noteable_ref, author, direction:)
- unless [:to, :from].include?(direction)
- raise ArgumentError, "Invalid direction `#{direction}`"
- end
-
- cross_reference = noteable_ref.to_reference(project)
- body = "moved #{direction} #{cross_reference}"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
- end
-
- # Called when a Noteable has been marked as a duplicate of another Issue
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # canonical_issue - Issue that this is a duplicate of
- #
- # Example Note text:
- #
- # "marked this issue as a duplicate of #1234"
- #
- # "marked this issue as a duplicate of other_project#5678"
- #
- # Returns the created Note object
- def mark_duplicate_issue(noteable, project, author, canonical_issue)
- body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}"
- create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
- end
-
- # Called when a Noteable has been marked as the canonical Issue of a duplicate
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # duplicate_issue - Issue that was a duplicate of this
- #
- # Example Note text:
- #
- # "marked #1234 as a duplicate of this issue"
- #
- # "marked other_project#5678 as a duplicate of this issue"
- #
- # Returns the created Note object
- def mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue)
- body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue"
- create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
+ def change_branch_presence(noteable, project, author, branch_type, branch, presence)
+ service = MergeRequestsSystemNoteService.new(noteable, project, author)
+ service.change_branch_presence(branch_type, branch, presence)
end
- def discussion_lock(issuable, author)
- action = issuable.discussion_locked? ? 'locked' : 'unlocked'
- body = "#{action} this #{issuable.class.to_s.titleize.downcase}"
-
- create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action))
+ def new_issue_branch(issue, project, author, branch, branch_project: nil)
+ service = MergeRequestsSystemNoteService.new(issue, project, author)
+ service.new_issue_branch(branch, branch_project: branch_project)
end
- def cross_reference?(note_text)
- note_text =~ /\A#{cross_reference_note_prefix}/i
+ def new_merge_request(issue, project, author, merge_request)
+ service = MergeRequestsSystemNoteService.new(issue, project, author)
+ service.new_merge_request(merge_request)
end
- def zoom_link_added(issue, project, author)
- create_note(NoteSummary.new(issue, project, author, _('added a Zoom call to this issue'), action: 'pinned_embed'))
+ def cross_reference(noteable, mentioner, author)
+ service = IssuablesSystemNoteService.new(noteable, nil, author)
+ service.cross_reference(mentioner)
end
- def zoom_link_removed(issue, project, author)
- create_note(NoteSummary.new(issue, project, author, _('removed a Zoom call from this issue'), action: 'pinned_embed'))
+ def cross_reference_exists?(noteable, mentioner)
+ service = IssuablesSystemNoteService.new(noteable, nil, nil)
+ service.cross_reference_exists?(mentioner)
end
- private
-
- # rubocop: disable CodeReuse/ActiveRecord
- def notes_for_mentioner(mentioner, noteable, notes)
- if mentioner.is_a?(Commit)
- text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
- notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize)
- else
- gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
- text = cross_reference_note_content(gfm_reference)
- notes.where(note: [text, text.capitalize])
- end
+ def change_task_status(noteable, project, author, new_task)
+ service = IssuablesSystemNoteService.new(noteable, project, author)
+ service.change_task_status(new_task)
end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def create_note(note_summary)
- note = Note.create(note_summary.note.merge(system: true))
- note.system_note_metadata = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
- note
+ def noteable_moved(noteable, project, noteable_ref, author, direction:)
+ service = IssuablesSystemNoteService.new(noteable, project, author)
+ service.noteable_moved(noteable_ref, direction)
end
- def cross_reference_note_prefix
- 'mentioned in '
+ def mark_duplicate_issue(noteable, project, author, canonical_issue)
+ service = IssuablesSystemNoteService.new(noteable, project, author)
+ service.mark_duplicate_issue(canonical_issue)
end
- def cross_reference_note_content(gfm_reference)
- "#{cross_reference_note_prefix}#{gfm_reference}"
+ def mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue)
+ service = IssuablesSystemNoteService.new(noteable, project, author)
+ service.mark_canonical_issue_of_duplicate(duplicate_issue)
end
- # Builds a list of existing and new commits according to existing_commits and
- # new_commits methods.
- # Returns a String wrapped in `ul` and `li` tags.
- def commits_list(noteable, new_commits, existing_commits, oldrev)
- existing_commit_summary = existing_commit_summary(noteable, existing_commits, oldrev)
- new_commit_summary = new_commit_summary(new_commits).join
-
- content_tag('ul', "#{existing_commit_summary}#{new_commit_summary}".html_safe)
+ def discussion_lock(issuable, author)
+ service = IssuablesSystemNoteService.new(issuable, issuable.project, author)
+ service.discussion_lock
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 a previous Commit
- #
- # 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 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 && !Gitlab::Git.blank_ref?(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?
-
- branch_name = content_tag('code', branch)
- content_tag('li', "#{commit_ids} - #{commits_text} from branch #{branch_name}".html_safe)
+ def cross_reference_disallowed?(noteable, mentioner)
+ service = IssuablesSystemNoteService.new(noteable, nil, nil)
+ service.cross_reference_disallowed?(mentioner)
end
- def url_helpers
- @url_helpers ||= Gitlab::Routing.url_helpers
+ def zoom_link_added(issue, project, author)
+ service = ZoomSystemNoteService.new(issue, project, author)
+ service.zoom_link_added
end
- def diff_comparison_path(merge_request, project, oldrev)
- diff_id = merge_request.merge_request_diff.id
-
- url_helpers.diffs_project_merge_request_path(
- project,
- merge_request,
- diff_id: diff_id,
- start_sha: oldrev
- )
+ def zoom_link_removed(issue, project, author)
+ service = ZoomSystemNoteService.new(issue, project, author)
+ service.zoom_link_removed
end
- def content_tag(*args)
- ActionController::Base.helpers.content_tag(*args)
+ # TODO: Just added for testing
+ def new_commit_summary(new_commits)
+ CommitSystemNoteService.new(nil, nil, nil).new_commit_summary(new_commits)
end
end
diff --git a/app/services/system_notes/base_system_note_service.rb b/app/services/system_notes/base_system_note_service.rb
new file mode 100644
index 00000000000..3abd82e9935
--- /dev/null
+++ b/app/services/system_notes/base_system_note_service.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class BaseSystemNoteService
+ attr_reader :noteable, :project, :author
+
+ def initialize(noteable, project, author)
+ @noteable = noteable
+ @project = project
+ @author = author
+ end
+
+ def create_note(note_summary)
+ note = Note.create(note_summary.note.merge(system: true))
+ note.system_note_metadata = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
+
+ note
+ end
+
+ def content_tag(*args)
+ ActionController::Base.helpers.content_tag(*args)
+ end
+
+ def url_helpers
+ @url_helpers ||= Gitlab::Routing.url_helpers
+ end
+end
diff --git a/app/services/system_notes/commit_system_notes_service.rb b/app/services/system_notes/commit_system_notes_service.rb
new file mode 100644
index 00000000000..fc64e0c8d66
--- /dev/null
+++ b/app/services/system_notes/commit_system_notes_service.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+class CommitSystemNoteService < BaseSystemNoteService
+ # 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 - Optional String SHA of a previous Commit
+ #
+ # See new_commit_summary and existing_commit_summary.
+ #
+ # Returns the created Note object
+ def add_commits(new_commits, existing_commits = [], oldrev = nil)
+ total_count = new_commits.length + existing_commits.length
+ commits_text = "#{total_count} commit".pluralize(total_count)
+
+ text_parts = ["added #{commits_text}"]
+ text_parts << commits_list(noteable, new_commits, existing_commits, oldrev)
+ text_parts << "[Compare with previous version](#{diff_comparison_path(noteable, project, oldrev)})"
+
+ body = text_parts.join("\n\n")
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count))
+ end
+
+ # Called when a commit was tagged
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the tag
+ # tag_name - The created tag name
+ #
+ # Returns the created Note object
+ def tag_commit(tag_name)
+ link = url_helpers.project_tag_path(project, id: tag_name)
+ body = "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'tag'))
+ 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 new_commit_summary(new_commits)
+ new_commits.collect do |commit|
+ content_tag('li', "#{commit.short_id} - #{commit.title}")
+ end
+ end
+
+ # Builds a list of existing and new commits according to existing_commits and
+ # new_commits methods.
+ # Returns a String wrapped in `ul` and `li` tags.
+ def commits_list(noteable, new_commits, existing_commits, oldrev)
+ existing_commit_summary = existing_commit_summary(noteable, existing_commits, oldrev)
+ new_commit_summary = new_commit_summary(new_commits).join
+
+ content_tag('ul', "#{existing_commit_summary}#{new_commit_summary}".html_safe)
+ end
+
+ private
+
+ # 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 a previous Commit
+ #
+ # 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 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 && !Gitlab::Git.blank_ref?(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?
+
+ branch_name = content_tag('code', branch)
+ content_tag('li', "#{commit_ids} - #{commits_text} from branch #{branch_name}".html_safe)
+ end
+
+ def diff_comparison_path(merge_request, project, oldrev)
+ diff_id = merge_request.merge_request_diff.id
+
+ url_helpers.diffs_project_merge_request_path(
+ project,
+ merge_request,
+ diff_id: diff_id,
+ start_sha: oldrev
+ )
+ end
+end
diff --git a/app/services/system_notes/issuables_system_note_service.rb b/app/services/system_notes/issuables_system_note_service.rb
new file mode 100644
index 00000000000..bd2fae5c65d
--- /dev/null
+++ b/app/services/system_notes/issuables_system_note_service.rb
@@ -0,0 +1,329 @@
+# frozen_string_literal: true
+
+class IssuablesSystemNoteService < BaseSystemNoteService
+ # Called when the assignee of a Noteable is changed or removed
+ #
+ # assignee - User being assigned, or nil
+ #
+ # Example Note text:
+ #
+ # "removed assignee"
+ #
+ # "assigned to @rspeicher"
+ #
+ # Returns the created Note object
+ def change_assignee(assignee)
+ body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
+ end
+
+ # Called when the assignees of an issuable is changed or removed
+ #
+ # assignees - Users being assigned, or nil
+ #
+ # Example Note text:
+ #
+ # "removed all assignees"
+ #
+ # "assigned to @user1 additionally to @user2"
+ #
+ # "assigned to @user1, @user2 and @user3 and unassigned from @user4 and @user5"
+ #
+ # "assigned to @user1 and @user2"
+ #
+ # Returns the created Note object
+ def change_issuable_assignees(old_assignees)
+ unassigned_users = old_assignees - noteable.assignees
+ added_users = noteable.assignees.to_a - old_assignees
+ text_parts = []
+
+ Gitlab::I18n.with_default_locale do
+ text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
+ text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
+ end
+
+ body = text_parts.join(' and ')
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
+ end
+
+ # Called when the milestone of a Noteable is changed
+ #
+ # milestone - Milestone being assigned, or nil
+ #
+ # Example Note text:
+ #
+ # "removed milestone"
+ #
+ # "changed milestone to 7.11"
+ #
+ # Returns the created Note object
+ def change_milestone(milestone)
+ format = milestone&.group_milestone? ? :name : :iid
+ body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
+ end
+
+ # Called when the title of a Noteable is changed
+ #
+ # old_title - Previous String title
+ #
+ # Example Note text:
+ #
+ # "changed title from **Old** to **New**"
+ #
+ # Returns the created Note object
+ def change_title(old_title)
+ new_title = noteable.title.dup
+
+ old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs
+
+ marked_old_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(old_title).mark(old_diffs, mode: :deletion)
+ marked_new_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(new_title).mark(new_diffs, mode: :addition)
+
+ body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ end
+
+ # Called when the description of a Noteable is changed
+ #
+ # noteable - Noteable object that responds to `description`
+ # project - Project owning noteable
+ # author - User performing the change
+ #
+ # Example Note text:
+ #
+ # "changed the description"
+ #
+ # Returns the created Note object
+ def change_description
+ body = 'changed the description'
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'description'))
+ 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 cross_reference(mentioner)
+ return if cross_reference_disallowed?(mentioner)
+
+ gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
+ body = cross_reference_note_content(gfm_reference)
+
+ if noteable.is_a?(ExternalIssue)
+ noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author)
+ else
+ create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))
+ end
+ 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. Additionally, it prevents the creation of references to
+ # external issues (which would fail).
+ #
+ # noteable - Noteable object being referenced
+ # mentioner - Mentionable object
+ #
+ # Returns Boolean
+ def cross_reference_disallowed?(mentioner)
+ return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
+ return false unless mentioner.is_a?(MergeRequest)
+ return false unless noteable.is_a?(Commit)
+
+ mentioner.commits.include?(noteable)
+ end
+
+ # Called when the status of a Task has changed
+ #
+ # new_task - TaskList::Item object.
+ #
+ # Example Note text:
+ #
+ # "marked the task Whatever as completed."
+ #
+ # Returns the created Note object
+ def change_task_status(new_task)
+ status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
+ body = "marked the task **#{new_task.source}** as #{status_label}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'task'))
+ end
+
+ # Called when noteable has been moved to another project
+ #
+ # direction - symbol, :to or :from
+ # noteable - Noteable object
+ # noteable_ref - Referenced noteable
+ # author - User performing the move
+ #
+ # Example Note text:
+ #
+ # "moved to some_namespace/project_new#11"
+ #
+ # Returns the created Note object
+ def noteable_moved(noteable_ref, direction)
+ unless [:to, :from].include?(direction)
+ raise ArgumentError, "Invalid direction `#{direction}`"
+ end
+
+ cross_reference = noteable_ref.to_reference(project)
+ body = "moved #{direction} #{cross_reference}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
+ end
+
+ # Called when the confidentiality changes
+ #
+ # issue - Issue object
+ # project - Project owning the issue
+ # author - User performing the change
+ #
+ # Example Note text:
+ #
+ # "made the issue confidential"
+ #
+ # Returns the created Note object
+ def change_issue_confidentiality
+ if noteable.confidential
+ body = 'made the issue confidential'
+ action = 'confidential'
+ else
+ body = 'made the issue visible to everyone'
+ action = 'visible'
+ end
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: action))
+ 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:
+ #
+ # "merged"
+ #
+ # "closed via bc17db76"
+ #
+ # Returns the created Note object
+ def change_status(status, source = nil)
+ body = status.dup
+ body << " via #{source.gfm_reference(project)}" if source
+
+ action = status == 'reopened' ? 'opened' : status
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: action))
+ end
+
+ # Check if a cross reference to a noteable from a mentioner already exists
+ #
+ # This method is used to prevent multiple notes being created for a mention
+ # when a issue is updated, for example. The method also calls notes_for_mentioner
+ # to check if the mentioner is a commit, and return matches only on commit hash
+ # instead of project + commit, to avoid repeated mentions from forks.
+ #
+ # noteable - Noteable object being referenced
+ # mentioner - Mentionable object
+ #
+ # Returns Boolean
+ def cross_reference_exists?(mentioner)
+ notes = noteable.notes.system
+ notes_for_mentioner(mentioner, noteable, notes).exists?
+ end
+
+ # Called when a Noteable has been marked as a duplicate of another Issue
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # canonical_issue - Issue that this is a duplicate of
+ #
+ # Example Note text:
+ #
+ # "marked this issue as a duplicate of #1234"
+ #
+ # "marked this issue as a duplicate of other_project#5678"
+ #
+ # Returns the created Note object
+ def mark_duplicate_issue(canonical_issue)
+ body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}"
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
+ end
+
+ # Called when a Noteable has been marked as the canonical Issue of a duplicate
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # duplicate_issue - Issue that was a duplicate of this
+ #
+ # Example Note text:
+ #
+ # "marked #1234 as a duplicate of this issue"
+ #
+ # "marked other_project#5678 as a duplicate of this issue"
+ #
+ # Returns the created Note object
+ def mark_canonical_issue_of_duplicate(duplicate_issue)
+ body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue"
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
+ end
+
+ def discussion_lock
+ action = noteable.discussion_locked? ? 'locked' : 'unlocked'
+ body = "#{action} this #{noteable.class.to_s.titleize.downcase}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: action))
+ end
+
+ private
+
+ def cross_reference_note_content(gfm_reference)
+ "#{self.class.cross_reference_note_prefix}#{gfm_reference}"
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def notes_for_mentioner(mentioner, noteable, notes)
+ if mentioner.is_a?(Commit)
+ text = "#{self.class.cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
+ notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize)
+ else
+ gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
+ text = cross_reference_note_content(gfm_reference)
+ notes.where(note: [text, text.capitalize])
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def self.cross_reference_note_prefix
+ 'mentioned in '
+ end
+
+ def self.cross_reference?(note_text)
+ note_text =~ /\A#{cross_reference_note_prefix}/i
+ end
+end
diff --git a/app/services/system_notes/merge_requests_system_note_service.rb b/app/services/system_notes/merge_requests_system_note_service.rb
new file mode 100644
index 00000000000..02f52b050a0
--- /dev/null
+++ b/app/services/system_notes/merge_requests_system_note_service.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+class MergeRequestsSystemNoteService < BaseSystemNoteService
+ # Called when 'merge when pipeline succeeds' is executed
+ def merge_when_pipeline_succeeds(sha)
+ body = "enabled an automatic merge when the pipeline for #{sha} succeeds"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ end
+
+ # Called when 'merge when pipeline succeeds' is canceled
+ def cancel_merge_when_pipeline_succeeds
+ body = 'canceled the automatic merge'
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ end
+
+ # Called when 'merge when pipeline succeeds' is aborted
+ def abort_merge_when_pipeline_succeeds(reason)
+ body = "aborted the automatic merge because #{reason}"
+
+ ##
+ # TODO: Abort message should be sent by the system, not a particular user.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63187.
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ end
+
+ def handle_merge_request_wip
+ prefix = noteable.work_in_progress? ? "marked" : "unmarked"
+
+ body = "#{prefix} as a **Work In Progress**"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ end
+
+ def add_merge_request_wip_from_commit(commit)
+ body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ end
+
+ def resolve_all_discussions
+ body = "resolved all threads"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'discussion'))
+ end
+
+ def discussion_continued_in_issue(discussion, issue)
+ body = "created #{issue.to_reference} to continue this discussion"
+ note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
+
+ note = Note.create(note_attributes.merge(system: true, created_at: issue.system_note_timestamp))
+ note.system_note_metadata = SystemNoteMetadata.new(action: 'discussion')
+
+ note
+ end
+
+ def diff_discussion_outdated(discussion, change_position)
+ merge_request = discussion.noteable
+ diff_refs = change_position.diff_refs
+ version_index = merge_request.merge_request_diffs.viewable.count
+ position_on_text = change_position.on_text?
+ text_parts = ["changed this #{position_on_text ? 'line' : 'file'} in"]
+
+ if version_params = merge_request.version_params_for(diff_refs)
+ repository = project.repository
+ anchor = position_on_text ? change_position.line_code(repository) : change_position.file_hash
+ url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: anchor))
+
+ text_parts << "[version #{version_index} of the diff](#{url})"
+ else
+ text_parts << "version #{version_index} of the diff"
+ end
+
+ body = text_parts.join(' ')
+ note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
+
+ note = Note.create(note_attributes.merge(system: true))
+ note.system_note_metadata = SystemNoteMetadata.new(action: 'outdated')
+
+ note
+ end
+
+ # Called when a branch in Noteable is changed
+ #
+ # branch_type - 'source' or 'target'
+ # old_branch - old branch name
+ # new_branch - new branch name
+ #
+ # Example Note text:
+ #
+ # "changed target branch from `Old` to `New`"
+ #
+ # Returns the created Note object
+ def change_branch(branch_type, old_branch, new_branch)
+ body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
+ end
+
+ # Called when a branch in Noteable is added or deleted
+ #
+ # branch_type - :source or :target
+ # branch - branch name
+ # presence - :add or :delete
+ #
+ # Example Note text:
+ #
+ # "restored target branch `feature`"
+ #
+ # Returns the created Note object
+ def change_branch_presence(branch_type, branch, presence)
+ verb =
+ if presence == :add
+ 'restored'
+ else
+ 'deleted'
+ end
+
+ body = "#{verb} #{branch_type} branch `#{branch}`"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
+ end
+
+ # Called when a branch is created from the 'new branch' button on a issue
+ # Example note text:
+ #
+ # "created branch `201-issue-branch-button`"
+ def new_issue_branch(branch, branch_project: nil)
+ branch_project ||= project
+ link = url_helpers.project_compare_path(branch_project, from: branch_project.default_branch, to: branch)
+
+ body = "created branch [`#{branch}`](#{link}) to address this issue"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
+ end
+
+ def new_merge_request(merge_request)
+ body = "created merge request #{merge_request.to_reference(project)} to address this issue"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ end
+end
diff --git a/app/services/system_notes/time_tracking_system_note_service.rb b/app/services/system_notes/time_tracking_system_note_service.rb
new file mode 100644
index 00000000000..7c82fa7309b
--- /dev/null
+++ b/app/services/system_notes/time_tracking_system_note_service.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+class TimeTrackingSystemNoteService < BaseSystemNoteService
+ # Called when the due_date of a Noteable is changed
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # due_date - Due date being assigned, or nil
+ #
+ # Example Note text:
+ #
+ # "removed due date"
+ #
+ # "changed due date to September 20, 2018"
+ #
+ # Returns the created Note object
+ def change_due_date(due_date)
+ body = due_date ? "changed due date to #{due_date.to_s(:long)}" : 'removed due date'
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'due_date'))
+ end
+
+ # Called when the estimated time of a Noteable is changed
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # time_estimate - Estimated time
+ #
+ # Example Note text:
+ #
+ # "removed time estimate"
+ #
+ # "changed time estimate to 3d 5h"
+ #
+ # Returns the created Note object
+ def change_time_estimate
+ parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
+ body = if noteable.time_estimate == 0
+ "removed time estimate"
+ else
+ "changed time estimate to #{parsed_time}"
+ end
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
+ end
+
+ # Called when the spent time of a Noteable is changed
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # time_spent - Spent time
+ #
+ # Example Note text:
+ #
+ # "removed time spent"
+ #
+ # "added 2h 30m of time spent"
+ #
+ # Returns the created Note object
+ def change_time_spent
+ time_spent = noteable.time_spent
+
+ if time_spent == :reset
+ body = "removed time spent"
+ else
+ spent_at = noteable.spent_at
+ parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
+ action = time_spent > 0 ? 'added' : 'subtracted'
+
+ text_parts = ["#{action} #{parsed_time} of time spent"]
+ text_parts << "at #{spent_at}" if spent_at
+ body = text_parts.join(' ')
+ end
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
+ end
+end
diff --git a/app/services/system_notes/zoom_system_note_service.rb b/app/services/system_notes/zoom_system_note_service.rb
new file mode 100644
index 00000000000..a145cc9c780
--- /dev/null
+++ b/app/services/system_notes/zoom_system_note_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class ZoomSystemNoteService < BaseSystemNoteService
+ def zoom_link_added
+ create_note(NoteSummary.new(noteable, project, author, _('added a Zoom call to this issue'), action: 'pinned_embed'))
+ end
+
+ def zoom_link_removed
+ create_note(NoteSummary.new(noteable, project, author, _('removed a Zoom call from this issue'), action: 'pinned_embed'))
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 910fe3b50b7..9c3270c4be6 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -555,7 +555,7 @@ describe SystemNoteService do
context 'when cross-reference disallowed' do
before do
- expect(described_class).to receive(:cross_reference_disallowed?).and_return(true)
+ expect_any_instance_of(IssuablesSystemNoteService).to receive(:cross_reference_disallowed?).and_return(true)
end
it 'returns nil' do
@@ -569,7 +569,7 @@ describe SystemNoteService do
context 'when cross-reference allowed' do
before do
- expect(described_class).to receive(:cross_reference_disallowed?).and_return(false)
+ expect_any_instance_of(IssuablesSystemNoteService).to receive(:cross_reference_disallowed?).and_return(false)
end
it_behaves_like 'a system note' do