Skip to content
Snippets Groups Projects
Verified Commit 08bbb9fc authored by Douwe Maan's avatar Douwe Maan Committed by Luke "Jared" Bennett
Browse files

Add option to start a new discussion on an MR

parent 8bdfee8b
No related branches found
No related tags found
No related merge requests found
Showing
with 466 additions and 341 deletions
Loading
Loading
@@ -5,7 +5,7 @@
let $commentButtonTemplate;
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
 
window.FilesCommentButton = (function() {
this.FilesCommentButton = (function() {
var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
 
COMMENT_BUTTON_CLASS = '.add-diff-note';
Loading
Loading
@@ -55,14 +55,19 @@ window.FilesCommentButton = (function() {
 
textFileElement = this.getTextFileElement($currentTarget);
buttonParentElement.append(this.buildButton({
discussionID: lineContentElement.attr('data-discussion-id'),
lineType: lineContentElement.attr('data-line-type'),
noteableType: textFileElement.attr('data-noteable-type'),
noteableID: textFileElement.attr('data-noteable-id'),
commitID: textFileElement.attr('data-commit-id'),
noteType: lineContentElement.attr('data-note-type'),
position: lineContentElement.attr('data-position'),
lineType: lineContentElement.attr('data-line-type'),
discussionID: lineContentElement.attr('data-discussion-id'),
lineCode: lineContentElement.attr('data-line-code')
// LegacyDiffNote
lineCode: lineContentElement.attr('data-line-code'),
// DiffNote
position: lineContentElement.attr('data-position')
}));
};
 
Loading
Loading
@@ -76,14 +81,19 @@ window.FilesCommentButton = (function() {
 
FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
return $commentButtonTemplate.clone().attr({
'data-discussion-id': buttonAttributes.discussionID,
'data-line-type': buttonAttributes.lineType,
'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID,
'data-note-type': buttonAttributes.noteType,
// LegacyDiffNote
'data-line-code': buttonAttributes.lineCode,
'data-position': buttonAttributes.position,
'data-discussion-id': buttonAttributes.discussionID,
'data-line-type': buttonAttributes.lineType
// DiffNote
'data-position': buttonAttributes.position
});
};
 
Loading
Loading
Loading
Loading
@@ -213,11 +213,7 @@ require('./task_list');
_this.last_fetched_at = data.last_fetched_at;
_this.setPollingInterval(data.notes.length);
return $.each(notes, function(i, note) {
if (note.discussion_html != null) {
return _this.renderDiscussionNote(note);
} else {
return _this.renderNote(note);
}
_this.renderNote(note);
});
};
})(this)
Loading
Loading
@@ -278,6 +274,10 @@ require('./task_list');
 
Notes.prototype.renderNote = function(note) {
var $notesList;
if (note.discussion_html != null) {
return this.renderDiscussionNote(note);
}
if (!note.valid) {
if (note.errors.commands_only) {
new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
Loading
Loading
@@ -323,9 +323,9 @@ require('./task_list');
return;
}
this.note_ids.push(note.id);
form = $("#new-discussion-note-form-" + note.discussion_id);
if ((note.original_discussion_id != null) && form.length === 0) {
form = $("#new-discussion-note-form-" + note.original_discussion_id);
form = $(".js-discussion-note-form[data-discussion-id='" + note.discussion_id + "']");
if (form.length === 0) {
form = $(".js-discussion-note-form[data-original-discussion-id='" + note.original_discussion_id + "']");
}
row = form.closest("tr");
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
Loading
Loading
@@ -334,8 +334,8 @@ require('./task_list');
note_html.renderGFM();
// is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']");
if (discussionContainer.length === 0) {
discussionContainer = $(".notes[data-original-discussion-id='" + note.original_discussion_id + "']");
}
if (discussionContainer.length === 0) {
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
Loading
Loading
@@ -363,7 +363,7 @@ require('./task_list');
// Add note to 'Changes' page discussions
discussionContainer.append(note_html);
// Init discussion on 'Discussion' page if it is merge request page
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
$('ul.main-notes-list').append(note.discussion_html).renderGFM();
}
} else {
Loading
Loading
@@ -456,6 +456,7 @@ require('./task_list');
form.find("#note_line_code").remove();
form.find("#note_position").remove();
form.find("#note_type").remove();
form.find("#note_in_reply_to_discussion_id").remove();
form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
return this.parentTimeline = form.parents('.timeline');
};
Loading
Loading
@@ -470,10 +471,24 @@ require('./task_list');
*/
 
Notes.prototype.setupNoteForm = function(form) {
var textarea;
var textarea, key;
new gl.GLForm(form);
textarea = form.find(".js-note-text");
return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]);
key = [
"Note",
form.find("#note_noteable_type").val(),
form.find("#note_noteable_id").val(),
form.find("#note_commit_id").val(),
form.find("#note_type").val(),
form.find("#in_reply_to_discussion_id").val(),
// LegacyDiffNote
form.find("#note_line_code").val(),
// DiffNote
form.find("#note_position").val()
];
return new Autosave(textarea, key);
};
 
/*
Loading
Loading
@@ -510,7 +525,7 @@ require('./task_list');
}
}
 
this.renderDiscussionNote(note);
this.renderNote(note);
// cleanup after successfully creating a diff/discussion note
this.removeDiscussionNoteForm($form);
};
Loading
Loading
@@ -727,23 +742,35 @@ require('./task_list');
 
Sets some hidden fields in the form.
 
Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
and "noteableId" data attributes set.
Note: dataHolder must have the "discussionId" and "lineCode" data attributes set.
*/
 
Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
// setup note target
form.attr('id', "new-discussion-note-form-" + (dataHolder.data("discussionId")));
var discussionID = dataHolder.data("discussionId");
form.attr('id', "new-discussion-note-form-" + discussionID);
form.attr("data-discussion-id", discussionID);
form.attr("data-original-discussion-id", dataHolder.data("originalDiscussionId") || discussionID);
form.attr("data-line-code", dataHolder.data("lineCode"));
form.find("#note_type").val(dataHolder.data("noteType"));
form.find("#line_type").val(dataHolder.data("lineType"));
form.find("#in_reply_to_discussion_id").val(dataHolder.data("originalDiscussionId"));
form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
form.find("#note_commit_id").val(dataHolder.data("commitId"));
form.find("#note_type").val(dataHolder.data("noteType"));
// LegacyDiffNote
form.find("#note_line_code").val(dataHolder.data("lineCode"));
// DiffNote
form.find("#note_position").val(dataHolder.attr("data-position"));
form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
form.find('.js-note-target-close').remove();
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
 
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
Loading
Loading
Loading
Loading
@@ -2,6 +2,7 @@
#
# Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController
include NotesHelper
include CreatesCommit
include DiffForPath
include DiffHelper
Loading
Loading
@@ -111,22 +112,19 @@ class Projects::CommitController < Projects::ApplicationController
end
 
def define_note_vars
@grouped_diff_discussions = commit.notes.grouped_diff_discussions
@notes = commit.notes.non_diff_notes.fresh
Banzai::NoteRenderer.render(
@grouped_diff_discussions.values.flat_map(&:notes) + @notes,
@project,
current_user,
)
@noteable = @commit
@note = @project.build_commit_note(commit)
 
@noteable = @commit
@comments_target = {
@new_diff_note_attrs = {
noteable_type: 'Commit',
commit_id: @commit.id
}
@grouped_diff_discussions = commit.grouped_diff_discussions
@discussions = commit.discussions
@notes = (@grouped_diff_discussions.values + @discussions).flat_map(&:notes)
@notes = prepare_notes_for_rendering(@notes)
end
 
def assign_change_commit_vars
Loading
Loading
Loading
Loading
@@ -28,7 +28,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
end
 
def discussion
@discussion ||= @merge_request.find_diff_discussion(params[:id]) || render_404
@discussion ||= @merge_request.find_discussion(params[:id]) || render_404
end
 
def authorize_resolve_discussion!
Loading
Loading
Loading
Loading
@@ -84,15 +84,11 @@ class Projects::IssuesController < Projects::ApplicationController
end
 
def show
raw_notes = @issue.notes.inc_relations_for_view.fresh
@notes = Banzai::NoteRenderer.
render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
@note = @project.notes.new(noteable: @issue)
@noteable = @issue
@note = @project.notes.new(noteable: @issue)
 
preload_max_access_for_authors(@notes, @project)
@discussions = @issue.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
 
respond_to do |format|
format.html
Loading
Loading
Loading
Loading
@@ -570,20 +570,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@note = @project.notes.new(noteable: @merge_request)
 
@discussions = @merge_request.discussions
preload_noteable_for_regular_notes(@discussions.flat_map(&:notes))
# This is not executed lazily
@notes = Banzai::NoteRenderer.render(
@discussions.flat_map(&:notes),
@project,
current_user,
@path,
@project_wiki,
@ref
)
preload_max_access_for_authors(@notes, @project)
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end
 
def define_widget_vars
Loading
Loading
@@ -596,22 +583,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
 
def define_diff_comment_vars
@comments_target = {
@new_diff_note_attrs = {
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
}
 
@use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
@grouped_diff_discussions = @merge_request.notes.inc_relations_for_view.grouped_diff_discussions
Banzai::NoteRenderer.render(
@grouped_diff_discussions.values.flat_map(&:notes),
@project,
current_user,
@path,
@project_wiki,
@ref
)
@grouped_diff_discussions = @merge_request.grouped_diff_discussions
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flat_map(&:notes))
end
 
def define_pipelines_vars
Loading
Loading
Loading
Loading
@@ -6,13 +6,14 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
before_action :find_current_user_notes, only: [:index]
 
def index
current_fetched_at = Time.now.to_i
 
notes_json = { notes: [], last_fetched_at: current_fetched_at }
 
@notes = notes_finder.execute.inc_author
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
 
Loading
Loading
@@ -23,7 +24,11 @@ class Projects::NotesController < Projects::ApplicationController
end
 
def create
create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha])
create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id],
new_discussion: params[:new_discussion],
)
@note = Notes::CreateService.new(project, current_user, create_params).execute
 
if @note.is_a?(Note)
Loading
Loading
@@ -111,6 +116,17 @@ class Projects::NotesController < Projects::ApplicationController
)
end
 
def discussion_html(discussion)
return if discussion.render_as_individual_notes?
render_to_string(
"discussions/_discussion",
layout: false,
formats: [:html],
locals: { discussion: discussion }
)
end
def diff_discussion_html(discussion)
return unless discussion.diff_discussion?
 
Loading
Loading
@@ -135,17 +151,6 @@ class Projects::NotesController < Projects::ApplicationController
)
end
 
def discussion_html(discussion)
return unless discussion.diff_discussion?
render_to_string(
"discussions/_discussion",
layout: false,
formats: [:html],
locals: { discussion: discussion }
)
end
def note_json(note)
attrs = {
id: note.id
Loading
Loading
@@ -156,33 +161,22 @@ class Projects::NotesController < Projects::ApplicationController
 
attrs.merge!(
valid: true,
discussion_id: note.discussion_id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
 
if note.diff_note?
discussion = note.to_discussion
discussion = note.to_discussion(noteable)
unless discussion.render_as_individual_notes?
attrs.merge!(
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
discussion_html: discussion_html(discussion),
 
# The discussion_id is used to add the comment to the correct discussion
# element on the merge request page. Among other things, the discussion_id
# contains the sha of head commit of the merge request.
# When new commits are pushed into the merge request after the initial
# load of the merge request page, the discussion elements will still have
# the old discussion_ids, with the old head commit sha. The new comment,
# however, will have the new discussion_id with the new commit sha.
# To ensure that these new comments will still end up in the correct
# discussion element, we also send the original discussion_id, with the
# old commit sha, along, and fall back on this value when no discussion
# element with the new discussion_id could be found.
if note.new_diff_note? && note.position != note.original_position
attrs[:original_discussion_id] = note.original_discussion_id
end
# Since the `discussion_id` can change, for example when new commits are pushed into an MR,
# the never-changing `original_discussion_id` is used as a fallback to the find the relevant
# discussion container to add this note to.
original_discussion_id: note.original_discussion_id
)
end
else
attrs.merge!(
Loading
Loading
@@ -205,14 +199,30 @@ class Projects::NotesController < Projects::ApplicationController
 
def note_params
params.require(:note).permit(
:note, :noteable, :noteable_id, :noteable_type, :project_id,
:attachment, :line_code, :commit_id, :type, :position
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
 
def find_current_user_notes
@notes = NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
.execute.inc_author
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
end
def noteable
@noteable ||= notes_finder.target
end
 
def last_fetched_at
Loading
Loading
class Projects::SnippetsController < Projects::ApplicationController
include NotesHelper
include ToggleAwardEmoji
include SpammableActions
include SnippetsActions
Loading
Loading
@@ -55,8 +56,10 @@ class Projects::SnippetsController < Projects::ApplicationController
 
def show
@note = @project.notes.new(noteable: @snippet)
@notes = Banzai::NoteRenderer.render(@snippet.notes.fresh, @project, current_user)
@noteable = @snippet
@discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end
 
def destroy
Loading
Loading
Loading
Loading
@@ -17,29 +17,46 @@ class NotesFinder
@project = project
@current_user = current_user
@params = params
init_collection
end
 
def execute
@notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at]
@notes = init_collection
@notes = since_fetch_at(@params[:last_fetched_at], @notes) if @params[:last_fetched_at]
@notes
end
 
private
def target
return @target if defined?(@target)
 
def init_collection
@notes =
if @params[:target_id]
on_target(@params[:target_type], @params[:target_id])
target_type = @params[:target_type]
target_id = @params[:target_id]
return @target = nil unless target_type && target_id
@target =
if target_type == "commit"
if Ability.allowed?(@current_user, :download_code, @project)
@project.commit(target_id)
end
else
notes_of_any_type
noteables_for_type(target_type).find(target_id)
end
end
 
private
def init_collection
if target
notes_on_target
else
notes_of_any_type
end
end
def notes_of_any_type
types = %w(commit issue merge_request snippet)
note_relations = types.map { |t| notes_for_type(t) }
note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search]
note_relations.map! { |notes| search(@params[:search], notes) } if @params[:search]
UnionFinder.new.find_union(note_relations, Note)
end
 
Loading
Loading
@@ -69,17 +86,11 @@ class NotesFinder
end
end
 
def on_target(target_type, target_id)
if target_type == "commit"
notes_for_type('commit').for_commit_id(target_id)
def notes_on_target
if target.respond_to?(:related_notes)
target.related_notes
else
target = noteables_for_type(target_type).find(target_id)
if target.respond_to?(:related_notes)
target.related_notes
else
target.notes
end
target.notes
end
end
 
Loading
Loading
@@ -94,10 +105,9 @@ class NotesFinder
 
# Notes changed since last fetch
# Uses overlapping intervals to avoid worrying about race conditions
def since_fetch_at(fetch_time)
def since_fetch_at(fetch_time, notes_relation = @notes)
# Default to 0 to remain compatible with old clients
last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i)
@notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
notes_relation.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
end
end
Loading
Loading
@@ -24,9 +24,9 @@ module NotesHelper
end
 
def diff_view_data
return {} unless @comments_target
return {} unless @new_diff_note_attrs
 
@comments_target.slice(:noteable_id, :noteable_type, :commit_id)
@new_diff_note_attrs.slice(:noteable_id, :noteable_type, :commit_id)
end
 
def diff_view_line_data(line_code, position, line_type)
Loading
Loading
@@ -53,37 +53,26 @@ module NotesHelper
}
 
if use_legacy_diff_note
discussion_id = LegacyDiffNote.discussion_id(
@comments_target[:noteable_type],
@comments_target[:noteable_id] || @comments_target[:commit_id],
line_code
)
data.merge!(
note_type: LegacyDiffNote.name,
discussion_id: discussion_id
)
new_note = LegacyDiffNote.new(@new_diff_note_attrs.merge(line_code: line_code))
discussion_id = new_note.discussion_id
else
discussion_id = DiffNote.discussion_id(
@comments_target[:noteable_type],
@comments_target[:noteable_id] || @comments_target[:commit_id],
position
)
data.merge!(
position: position.to_json,
note_type: DiffNote.name,
discussion_id: discussion_id
)
new_note = DiffNote.new(@new_diff_note_attrs.merge(position: position))
discussion_id = new_note.discussion_id
data[:position] = position.to_json
end
 
data
data.merge(
note_type: new_note.type,
discussion_id: discussion_id
)
end
 
def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user
 
data = discussion.reply_attributes.merge(line_type: line_type)
data = { discussion_id: discussion.id, original_discussion_id: discussion.original_id, line_type: line_type }
data[:line_code] = discussion.line_code if discussion.respond_to?(:line_code)
 
button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
data: data, title: 'Add a reply'
Loading
Loading
@@ -95,7 +84,15 @@ module NotesHelper
end
 
def preload_noteable_for_regular_notes(notes)
ActiveRecord::Associations::Preloader.new.preload(notes.select { |note| !note.for_commit? }, :noteable)
ActiveRecord::Associations::Preloader.new.preload(notes.reject(&:for_commit?), :noteable)
end
def prepare_notes_for_rendering(notes)
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
Banzai::NoteRenderer.render(notes, @project, current_user)
notes
end
 
def note_max_access_for_user(note)
Loading
Loading
Loading
Loading
@@ -2,6 +2,7 @@ class Commit
extend ActiveModel::Naming
 
include ActiveModel::Conversion
include Noteable
include Participable
include Mentionable
include Referable
Loading
Loading
@@ -203,6 +204,10 @@ class Commit
project.notes.for_commit_id(self.id)
end
 
def discussion_notes
notes.non_diff_notes
end
def notes_with_associations
notes.includes(:author)
end
Loading
Loading
class CommitDiscussion < Discussion
def self.override_discussion_id(note)
discussion_id(note)
end
def potentially_resolvable?
false
end
end
Loading
Loading
@@ -24,12 +24,4 @@ module NoteOnDiff
def diff_attributes
raise NotImplementedError
end
def can_be_award_emoji?
false
end
def to_discussion
Discussion.new([self])
end
end
module Noteable
def discussion_notes
notes
end
delegate :find_discussion, :find_original_discussion, to: :discussion_notes
def discussions
@discussions ||= discussion_notes
.inc_relations_for_view
.discussions(self)
end
def grouped_diff_discussions
notes.inc_relations_for_view.grouped_diff_discussions
end
end
module ResolvableNote
extend ActiveSupport::Concern
included do
belongs_to :resolved_by, class_name: "User"
validates :resolved_by, presence: true, if: :resolved?
# Keep this scope in sync with the logic in `#resolvable?` in `Note` subclasses that are resolvable
scope :resolvable, -> { where(type: %w(DiffNote DiscussionNote)).user.where(noteable_type: 'MergeRequest') }
scope :resolved, -> { resolvable.where.not(resolved_at: nil) }
scope :unresolved, -> { resolvable.where(resolved_at: nil) }
end
module ClassMethods
# This method must be kept in sync with `#resolve!`
def resolve!(current_user)
unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id)
end
# This method must be kept in sync with `#unresolve!`
def unresolve!
resolved.update_all(resolved_at: nil, resolved_by_id: nil)
end
end
# If you update this method remember to also update the scope `resolvable`
def resolvable?
to_discussion.potentially_resolvable? && !system?
end
def resolved?
return false unless resolvable?
self.resolved_at.present?
end
def to_be_resolved?
resolvable? && !resolved?
end
# If you update this method remember to also update `.resolve!`
def resolve!(current_user)
return unless resolvable?
return if resolved?
self.resolved_at = Time.now
self.resolved_by = current_user
save!
end
# If you update this method remember to also update `.unresolve!`
def unresolve!
return unless resolvable?
return unless resolved?
self.resolved_at = nil
self.resolved_by = nil
save!
end
end
class DiffDiscussion < Discussion
NUMBER_OF_TRUNCATED_DIFF_LINES = 16
delegate :line_code,
:original_line_code,
:diff_file,
:for_line?,
:active?,
to: :first_note
delegate :blob,
:highlighted_diff_lines,
:diff_lines,
to: :diff_file,
allow_nil: true
def self.build_discussion_id(note)
[*super(note), *unique_position_identifier(note)]
end
def self.build_original_discussion_id(note)
[*Discussion.build_discussion_id(note), *note.original_position.key]
end
def self.unique_position_identifier(note)
note.position.key
end
def diff_discussion?
true
end
def legacy_diff_discussion?
false
end
def active?
return @active if @active.present?
@active = first_note.active?
end
MEMOIZED_VALUES << :active
def reply_attributes
super.merge(first_note.diff_attributes)
end
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines
prev_lines = []
lines.each do |line|
if line.meta?
prev_lines.clear
else
prev_lines << line
break if for_line?(line)
prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
end
end
prev_lines
end
end
Loading
Loading
@@ -9,58 +9,44 @@ class DiffNote < Note
validates :diff_line, presence: true
validates :line_code, presence: true, line_code: true
validates :noteable_type, inclusion: { in: %w(Commit MergeRequest) }
validates :resolved_by, presence: true, if: :resolved?
validate :positions_complete
validate :verify_supported
 
# Keep this scope in sync with the logic in `#resolvable?`
scope :resolvable, -> { user.where(noteable_type: 'MergeRequest') }
scope :resolved, -> { resolvable.where.not(resolved_at: nil) }
scope :unresolved, -> { resolvable.where(resolved_at: nil) }
after_initialize :ensure_original_discussion_id
before_validation :set_original_position, :update_position, on: :create
before_validation :set_line_code, :set_original_discussion_id
before_validation :set_line_code
# We need to do this again, because it's already in `Note`, but is affected by
# `update_position` and needs to run after that.
before_validation :set_discussion_id
before_validation :set_discussion_id, if: :position_changed?
after_save :keep_around_commits
 
class << self
def build_discussion_id(noteable_type, noteable_id, position)
[super(noteable_type, noteable_id), *position.key].join("-")
end
# This method must be kept in sync with `#resolve!`
def resolve!(current_user)
unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id)
end
# This method must be kept in sync with `#unresolve!`
def unresolve!
resolved.update_all(resolved_at: nil, resolved_by_id: nil)
end
end
def new_diff_note?
true
end
 
def discussion_class(*)
DiffDiscussion
end
def diff_attributes
{ position: position.to_json }
{
original_position: original_position.to_json,
position: position.to_json,
}
end
 
def position=(new_position)
if new_position.is_a?(String)
new_position = JSON.parse(new_position) rescue nil
end
%i(original_position= position=).each do |meth|
define_method meth do |new_position|
if new_position.is_a?(String)
new_position = JSON.parse(new_position) rescue nil
end
 
if new_position.is_a?(Hash)
new_position = new_position.with_indifferent_access
new_position = Gitlab::Diff::Position.new(new_position)
end
if new_position.is_a?(Hash)
new_position = new_position.with_indifferent_access
new_position = Gitlab::Diff::Position.new(new_position)
end
 
super(new_position)
super(new_position)
end
end
 
def diff_file
Loading
Loading
@@ -88,43 +74,6 @@ class DiffNote < Note
self.position.diff_refs == diff_refs
end
 
# If you update this method remember to also update the scope `resolvable`
def resolvable?
!system? && for_merge_request?
end
def resolved?
return false unless resolvable?
self.resolved_at.present?
end
# If you update this method remember to also update `.resolve!`
def resolve!(current_user)
return unless resolvable?
return if resolved?
self.resolved_at = Time.now
self.resolved_by = current_user
save!
end
# If you update this method remember to also update `.unresolve!`
def unresolve!
return unless resolvable?
return unless resolved?
self.resolved_at = nil
self.resolved_by = nil
save!
end
def discussion
return unless resolvable?
self.noteable.find_diff_discussion(self.discussion_id)
end
private
 
def supported?
Loading
Loading
@@ -140,33 +89,13 @@ class DiffNote < Note
end
 
def set_original_position
self.original_position = self.position.dup
self.original_position = self.position.dup unless self.original_position&.complete?
end
 
def set_line_code
self.line_code = self.position.line_code(self.project.repository)
end
 
def ensure_original_discussion_id
return unless self.persisted?
return if self.original_discussion_id
set_original_discussion_id
update_column(:original_discussion_id, self.original_discussion_id)
end
def set_original_discussion_id
self.original_discussion_id = Digest::SHA1.hexdigest(build_original_discussion_id)
end
def build_discussion_id
self.class.build_discussion_id(noteable_type, noteable_id || commit_id, position)
end
def build_original_discussion_id
self.class.build_discussion_id(noteable_type, noteable_id || commit_id, original_position)
end
def update_position
return unless supported?
return if for_commit?
Loading
Loading
class Discussion
NUMBER_OF_TRUNCATED_DIFF_LINES = 16
MEMOIZED_VALUES = [] # rubocop:disable Style/MutableConstant
 
attr_reader :notes
 
Loading
Loading
@@ -11,12 +11,6 @@ class Discussion
:for_commit?,
:for_merge_request?,
 
:line_code,
:original_line_code,
:diff_file,
:for_line?,
:active?,
to: :first_note
 
delegate :resolved_at,
Loading
Loading
@@ -25,29 +19,46 @@ class Discussion
to: :last_resolved_note,
allow_nil: true
 
delegate :blob,
:highlighted_diff_lines,
:diff_lines,
def self.build(notes, noteable = nil)
notes.first.discussion_class(noteable).new(notes, noteable)
end
 
to: :diff_file,
allow_nil: true
def self.build_collection(notes, noteable = nil)
notes.group_by { |n| n.discussion_id(noteable) }.values.map { |notes| build(notes, noteable) }
end
 
def self.for_notes(notes)
notes.group_by(&:discussion_id).values.map { |notes| new(notes) }
def self.discussion_id(note)
Digest::SHA1.hexdigest(build_discussion_id(note).join("-"))
end
 
def self.for_diff_notes(notes)
notes.group_by(&:line_code).values.map { |notes| new(notes) }
# Optionally override the discussion ID at runtime depending on circumstances
def self.override_discussion_id(note)
nil
end
 
def initialize(notes)
@notes = notes
def self.build_discussion_id(note)
noteable_id = note.noteable_id || note.commit_id
[:discussion, note.noteable_type.try(:underscore), noteable_id]
end
 
def last_resolved_note
return unless resolved?
def self.original_discussion_id(note)
original_discussion_id = build_original_discussion_id(note)
if original_discussion_id
Digest::SHA1.hexdigest(original_discussion_id.join("-"))
else
note.discussion_id
end
end
 
@last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last
# Optionally build a separate original discussion ID that will never change,
# if the main discussion ID _can_ change, like in the case of DiffDiscussion.
def self.build_original_discussion_id(note)
nil
end
def initialize(notes, noteable = nil)
@notes = notes
@noteable = noteable
end
 
def last_updated_at
Loading
Loading
@@ -59,42 +70,64 @@ class Discussion
end
 
def id
first_note.discussion_id
first_note.discussion_id(noteable)
end
 
alias_method :to_param, :id
 
def original_id
first_note.original_discussion_id
end
def diff_discussion?
first_note.diff_note?
false
end
def render_as_individual_notes?
false
end
 
def legacy_diff_discussion?
notes.any?(&:legacy_diff_note?)
def potentially_resolvable?
first_note.for_merge_request?
end
 
def resolvable?
return @resolvable if @resolvable.present?
 
@resolvable = diff_discussion? && notes.any?(&:resolvable?)
@resolvable = potentially_resolvable? && notes.any?(&:resolvable?)
end
MEMOIZED_VALUES << :resolvable
 
def resolved?
return @resolved if @resolved.present?
 
@resolved = resolvable? && notes.none?(&:to_be_resolved?)
end
MEMOIZED_VALUES << :resolved
 
def first_note
@first_note ||= @notes.first
@first_note ||= notes.first
end
MEMOIZED_VALUES << :first_note
 
def first_note_to_resolve
@first_note_to_resolve ||= notes.detect(&:to_be_resolved?)
return unless resolvable?
@first_note_to_resolve ||= notes.find(&:to_be_resolved?)
end
MEMOIZED_VALUES << :first_note_to_resolve
def last_resolved_note
return unless resolved?
@last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last
end
MEMOIZED_VALUES << :last_resolved_note
 
def last_note
@last_note ||= @notes.last
@last_note ||= notes.last
end
MEMOIZED_VALUES << :last_note
 
def resolved_notes
notes.select(&:resolved?)
Loading
Loading
@@ -124,25 +157,12 @@ class Discussion
update { |notes| notes.unresolve! }
end
 
def for_target?(target)
self.noteable == target && !diff_discussion?
end
def active?
return @active if @active.present?
@active = first_note.active?
end
def collapsed?
return false unless diff_discussion?
if resolvable?
# New diff discussions only disappear once they are marked resolved
resolved?
else
# Old diff discussions disappear once they become outdated
!active?
false
end
end
 
Loading
Loading
@@ -151,52 +171,22 @@ class Discussion
end
 
def reply_attributes
data = {
noteable_type: first_note.noteable_type,
noteable_id: first_note.noteable_id,
commit_id: first_note.commit_id,
discussion_id: self.id,
}
if diff_discussion?
data[:note_type] = first_note.type
data.merge!(first_note.diff_attributes)
end
data
end
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines
prev_lines = []
lines.each do |line|
if line.meta?
prev_lines.clear
else
prev_lines << line
break if for_line?(line)
prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
end
end
prev_lines
first_note.slice(:type, :noteable_type, :noteable_id, :commit_id)
end
 
private
 
def update
notes_relation = DiffNote.where(id: notes.map(&:id)).fresh
# Do not select `Note.resolvable`, so that system notes remain in the collection
notes_relation = Note.where(id: notes.map(&:id))
yield(notes_relation)
 
# Set the notes array to the updated notes
@notes = notes_relation.to_a
@notes = notes_relation.fresh.to_a
 
# Reset the memoized values
@last_resolved_note = @resolvable = @resolved = @first_note = @last_note = nil
MEMOIZED_VALUES.each do |var|
instance_variable_set(:"@#{var}", nil)
end
end
end
class DiscussionNote < Note
NOTEABLE_TYPES = %w(MergeRequest).freeze
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
def discussion_class(*)
SimpleDiscussion
end
end
class IndividualNoteDiscussion < Discussion
def self.build_discussion_id(note)
[*super(note), SecureRandom.hex]
end
def potentially_resolvable?
false
end
def render_as_individual_notes?
true
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment