diff --git a/CHANGELOG b/CHANGELOG index d8c57441b8baeb720bc243202ca6f9c71ceb0ab4..383ccc38c564e736bbebb0f251843d4b0777d72a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -111,6 +111,7 @@ v 8.11.5 - Optimize branch lookups and force a repository reload for Repository#find_branch. !6087 - Fix member expiration date picker after update. !6184 - Fix suggested colors options for new labels in the admin area. !6138 + - Optimize discussion notes resolving and unresolving - Fix GitLab import button - Fix confidential issues being exposed as public using gitlab.com export - Remove gitorious from import_sources. !6180 diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 4442cefc7e955c255c921e8ecd02afa7a33e6ab3..559b30759050c58305cedbba8ace0df22e78fa51 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -13,6 +13,11 @@ class DiffNote < Note 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 @@ -25,6 +30,16 @@ class DiffNote < Note 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? @@ -73,6 +88,7 @@ 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 @@ -83,6 +99,7 @@ class DiffNote < Note 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? @@ -92,6 +109,7 @@ class DiffNote < Note save! end + # If you update this method remember to also update `.unresolve!` def unresolve! return unless resolvable? return unless resolved? diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 9676bc034708630130e3fe79b8d63f3c47830f74..de06c13481a1899aa80110139bbd6b700f6091b7 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -1,7 +1,7 @@ class Discussion NUMBER_OF_TRUNCATED_DIFF_LINES = 16 - attr_reader :first_note, :last_note, :notes + attr_reader :notes delegate :created_at, :project, @@ -36,8 +36,6 @@ class Discussion end def initialize(notes) - @first_note = notes.first - @last_note = notes.last @notes = notes end @@ -70,17 +68,25 @@ class Discussion end def resolvable? - return @resolvable if defined?(@resolvable) + return @resolvable if @resolvable.present? @resolvable = diff_discussion? && notes.any?(&:resolvable?) end def resolved? - return @resolved if defined?(@resolved) + return @resolved if @resolved.present? @resolved = resolvable? && notes.none?(&:to_be_resolved?) end + def first_note + @first_note ||= @notes.first + end + + def last_note + @last_note ||= @notes.last + end + def resolved_notes notes.select(&:resolved?) end @@ -100,17 +106,13 @@ class Discussion def resolve!(current_user) return unless resolvable? - notes.each do |note| - note.resolve!(current_user) if note.resolvable? - end + update { |notes| notes.resolve!(current_user) } end def unresolve! return unless resolvable? - notes.each do |note| - note.unresolve! if note.resolvable? - end + update { |notes| notes.unresolve! } end def for_target?(target) @@ -118,7 +120,7 @@ class Discussion end def active? - return @active if defined?(@active) + return @active if @active.present? @active = first_note.active? end @@ -174,4 +176,17 @@ class Discussion prev_lines end + + private + + def update + notes_relation = DiffNote.where(id: notes.map(&:id)).fresh + yield(notes_relation) + + # Set the notes array to the updated notes + @notes = notes_relation.to_a + + # Reset the memoized values + @last_resolved_note = @resolvable = @resolved = @first_note = @last_note = nil + end end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 83e38095febed99cdbce6c06bb20168917117518..6919002dedcd439a03f34a9a077916b7272071cd 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -28,6 +28,11 @@ FactoryGirl.define do diff_refs: noteable.diff_refs ) end + + trait :resolved do + resolved_at { Time.now } + resolved_by { create(:user) } + end end factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index 6a640474cfe04f6b057af2f4c91e514b359db77e..3db5937a4f3932eec5c2354fbb372e79ebe7fb33 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -31,6 +31,43 @@ describe DiffNote, models: true do subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } + describe ".resolve!" do + let(:current_user) { create(:user) } + let!(:commit_note) { create(:diff_note_on_commit) } + let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) } + let!(:unresolved_note) { create(:diff_note_on_merge_request) } + + before do + described_class.resolve!(current_user) + + commit_note.reload + resolved_note.reload + unresolved_note.reload + end + + it 'resolves only the resolvable, not yet resolved notes' do + expect(commit_note.resolved_at).to be_nil + expect(resolved_note.resolved_by).not_to eq(current_user) + expect(unresolved_note.resolved_at).not_to be_nil + expect(unresolved_note.resolved_by).to eq(current_user) + end + end + + describe ".unresolve!" do + let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) } + + before do + described_class.unresolve! + + resolved_note.reload + end + + it 'unresolves the resolved notes' do + expect(resolved_note.resolved_by).to be_nil + expect(resolved_note.resolved_at).to be_nil + end + end + describe "#position=" do context "when provided a string" do it "sets the position" do diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb index 179f2e7366247a1b684035e0ff9045f730d3f6b8..0142706d140039b54b98fa3e3e1e713713bfac3e 100644 --- a/spec/models/discussion_spec.rb +++ b/spec/models/discussion_spec.rb @@ -238,27 +238,19 @@ describe Discussion, model: true do context "when resolvable" do let(:user) { create(:user) } + let(:second_note) { create(:diff_note_on_commit) } # unresolvable before do allow(subject).to receive(:resolvable?).and_return(true) - - allow(first_note).to receive(:resolvable?).and_return(true) - allow(second_note).to receive(:resolvable?).and_return(false) - allow(third_note).to receive(:resolvable?).and_return(true) end context "when all resolvable notes are resolved" do before do first_note.resolve!(user) third_note.resolve!(user) - end - it "calls resolve! on every resolvable note" do - expect(first_note).to receive(:resolve!).with(current_user) - expect(second_note).not_to receive(:resolve!) - expect(third_note).to receive(:resolve!).with(current_user) - - subject.resolve!(current_user) + first_note.reload + third_note.reload end it "doesn't change resolved_at on the resolved notes" do @@ -309,46 +301,44 @@ describe Discussion, model: true do first_note.resolve!(user) end - it "calls resolve! on every resolvable note" do - expect(first_note).to receive(:resolve!).with(current_user) - expect(second_note).not_to receive(:resolve!) - expect(third_note).to receive(:resolve!).with(current_user) - - subject.resolve!(current_user) - end - it "doesn't change resolved_at on the resolved note" do expect(first_note.resolved_at).not_to be_nil - expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at } + expect { subject.resolve!(current_user) }. + not_to change { first_note.reload.resolved_at } end it "doesn't change resolved_by on the resolved note" do expect(first_note.resolved_by).to eq(user) - expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by } + expect { subject.resolve!(current_user) }. + not_to change { first_note.reload && first_note.resolved_by } end it "doesn't change the resolved state on the resolved note" do expect(first_note.resolved?).to be true - expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? } + expect { subject.resolve!(current_user) }. + not_to change { first_note.reload && first_note.resolved? } end it "sets resolved_at on the unresolved note" do subject.resolve!(current_user) + third_note.reload expect(third_note.resolved_at).not_to be_nil end it "sets resolved_by on the unresolved note" do subject.resolve!(current_user) + third_note.reload expect(third_note.resolved_by).to eq(current_user) end it "marks the unresolved note as resolved" do subject.resolve!(current_user) + third_note.reload expect(third_note.resolved?).to be true end @@ -373,16 +363,10 @@ describe Discussion, model: true do end context "when no resolvable notes are resolved" do - it "calls resolve! on every resolvable note" do - expect(first_note).to receive(:resolve!).with(current_user) - expect(second_note).not_to receive(:resolve!) - expect(third_note).to receive(:resolve!).with(current_user) - - subject.resolve!(current_user) - end - it "sets resolved_at on the unresolved notes" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(first_note.resolved_at).not_to be_nil expect(third_note.resolved_at).not_to be_nil @@ -390,6 +374,8 @@ describe Discussion, model: true do it "sets resolved_by on the unresolved notes" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(first_note.resolved_by).to eq(current_user) expect(third_note.resolved_by).to eq(current_user) @@ -397,6 +383,8 @@ describe Discussion, model: true do it "marks the unresolved notes as resolved" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(first_note.resolved?).to be true expect(third_note.resolved?).to be true @@ -404,18 +392,24 @@ describe Discussion, model: true do it "sets resolved_at" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(subject.resolved_at).not_to be_nil end it "sets resolved_by" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(subject.resolved_by).to eq(current_user) end it "marks as resolved" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(subject.resolved?).to be true end @@ -451,16 +445,10 @@ describe Discussion, model: true do third_note.resolve!(user) end - it "calls unresolve! on every resolvable note" do - expect(first_note).to receive(:unresolve!) - expect(second_note).not_to receive(:unresolve!) - expect(third_note).to receive(:unresolve!) - - subject.unresolve! - end - it "unsets resolved_at on the resolved notes" do subject.unresolve! + first_note.reload + third_note.reload expect(first_note.resolved_at).to be_nil expect(third_note.resolved_at).to be_nil @@ -468,6 +456,8 @@ describe Discussion, model: true do it "unsets resolved_by on the resolved notes" do subject.unresolve! + first_note.reload + third_note.reload expect(first_note.resolved_by).to be_nil expect(third_note.resolved_by).to be_nil @@ -475,6 +465,8 @@ describe Discussion, model: true do it "unmarks the resolved notes as resolved" do subject.unresolve! + first_note.reload + third_note.reload expect(first_note.resolved?).to be false expect(third_note.resolved?).to be false @@ -482,12 +474,16 @@ describe Discussion, model: true do it "unsets resolved_at" do subject.unresolve! + first_note.reload + third_note.reload expect(subject.resolved_at).to be_nil end it "unsets resolved_by" do subject.unresolve! + first_note.reload + third_note.reload expect(subject.resolved_by).to be_nil end @@ -504,40 +500,22 @@ describe Discussion, model: true do first_note.resolve!(user) end - it "calls unresolve! on every resolvable note" do - expect(first_note).to receive(:unresolve!) - expect(second_note).not_to receive(:unresolve!) - expect(third_note).to receive(:unresolve!) - - subject.unresolve! - end - it "unsets resolved_at on the resolved note" do subject.unresolve! - expect(first_note.resolved_at).to be_nil + expect(subject.first_note.resolved_at).to be_nil end it "unsets resolved_by on the resolved note" do subject.unresolve! - expect(first_note.resolved_by).to be_nil + expect(subject.first_note.resolved_by).to be_nil end it "unmarks the resolved note as resolved" do subject.unresolve! - expect(first_note.resolved?).to be false - end - end - - context "when no resolvable notes are resolved" do - it "calls unresolve! on every resolvable note" do - expect(first_note).to receive(:unresolve!) - expect(second_note).not_to receive(:unresolve!) - expect(third_note).to receive(:unresolve!) - - subject.unresolve! + expect(subject.first_note.resolved?).to be false end end end