Commit bf9732ce authored by Jan Provaznik's avatar Jan Provaznik

Merge branch '30782-store-issue-mr-description-changes' into 'master'

Store description versions

See merge request gitlab-org/gitlab!17147
parents 191e65f1 7a0304ad
......@@ -25,6 +25,7 @@ module Issuable
include UpdatedAtFilterable
include IssuableStates
include ClosedAtFilterable
include VersionedDescription
TITLE_LENGTH_MAX = 255
TITLE_HTML_LENGTH_MAX = 800
......
# frozen_string_literal: true
module VersionedDescription
extend ActiveSupport::Concern
included do
attr_accessor :saved_description_version
has_many :description_versions
after_update :save_description_version
end
private
def save_description_version
self.saved_description_version = nil
return unless Feature.enabled?(:save_description_versions, issuing_parent)
return unless saved_change_to_description?
unless description_versions.exists?
description_versions.create!(
description: description_before_last_save,
created_at: created_at
)
end
self.saved_description_version = description_versions.create!(description: description)
end
end
# frozen_string_literal: true
class DescriptionVersion < ApplicationRecord
belongs_to :issue
belongs_to :merge_request
validate :exactly_one_issuable
def self.issuable_attrs
%i(issue merge_request).freeze
end
private
def exactly_one_issuable
issuable_count = self.class.issuable_attrs.count { |attr| self["#{attr}_id"] }
errors.add(:base, "Exactly one of #{self.class.issuable_attrs.join(', ')} is required") if issuable_count != 1
end
end
DescriptionVersion.prepend_if_ee('EE::DescriptionVersion')
......@@ -23,6 +23,7 @@ class SystemNoteMetadata < ApplicationRecord
validates :action, inclusion: { in: :icon_types }, allow_nil: true
belongs_to :note
belongs_to :description_version
def icon_types
ICON_TYPES
......
......@@ -39,6 +39,10 @@ module Issuable
if note.system_note_metadata
new_params[:system_note_metadata] = note.system_note_metadata.dup
# TODO: Implement copying of description versions when an issue is moved
# https://gitlab.com/gitlab-org/gitlab/issues/32300
new_params[:system_note_metadata].description_version = nil
end
new_note.update(new_params)
......
......@@ -10,6 +10,10 @@ class NoteSummary
project: project, author: author, note: body }
@metadata = { action: action, commit_count: commit_count }.compact
if action == 'description' && noteable.saved_description_version
@metadata[:description_version] = noteable.saved_description_version
end
set_commit_params if note[:noteable].is_a?(Commit)
end
......
# frozen_string_literal: true
class CreateDescriptionVersions < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
create_table :description_versions do |t|
t.timestamps_with_timezone
t.references :issue, index: false, foreign_key: { on_delete: :cascade }, type: :integer
t.references :merge_request, index: false, foreign_key: { on_delete: :cascade }, type: :integer
t.references :epic, index: false, foreign_key: { on_delete: :cascade }, type: :integer
t.text :description
end
add_index :description_versions, :issue_id, where: 'issue_id IS NOT NULL'
add_index :description_versions, :merge_request_id, where: 'merge_request_id IS NOT NULL'
add_index :description_versions, :epic_id, where: 'epic_id IS NOT NULL'
add_column :system_note_metadata, :description_version_id, :bigint
end
def down
remove_column :system_note_metadata, :description_version_id
drop_table :description_versions
end
end
# frozen_string_literal: true
class AddIndexToSytemNoteMetadataDescriptionVersionId < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :system_note_metadata, :description_version_id, unique: true, where: 'description_version_id IS NOT NULL'
add_concurrent_foreign_key :system_note_metadata, :description_versions, column: :description_version_id, on_delete: :nullify
end
def down
remove_foreign_key :system_note_metadata, column: :description_version_id
remove_concurrent_index :system_note_metadata, :description_version_id
end
end
......@@ -1266,6 +1266,18 @@ ActiveRecord::Schema.define(version: 2019_10_16_220135) do
t.index ["project_id", "status"], name: "index_deployments_on_project_id_and_status"
end
create_table "description_versions", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "issue_id"
t.integer "merge_request_id"
t.integer "epic_id"
t.text "description"
t.index ["epic_id"], name: "index_description_versions_on_epic_id", where: "(epic_id IS NOT NULL)"
t.index ["issue_id"], name: "index_description_versions_on_issue_id", where: "(issue_id IS NOT NULL)"
t.index ["merge_request_id"], name: "index_description_versions_on_merge_request_id", where: "(merge_request_id IS NOT NULL)"
end
create_table "design_management_designs", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "issue_id"
......@@ -3494,6 +3506,8 @@ ActiveRecord::Schema.define(version: 2019_10_16_220135) do
t.string "action"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "description_version_id"
t.index ["description_version_id"], name: "index_system_note_metadata_on_description_version_id", unique: true, where: "(description_version_id IS NOT NULL)"
t.index ["note_id"], name: "index_system_note_metadata_on_note_id", unique: true
end
......@@ -4089,6 +4103,9 @@ ActiveRecord::Schema.define(version: 2019_10_16_220135) do
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "clusters", name: "fk_289bba3222", on_delete: :nullify
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
add_foreign_key "description_versions", "epics", on_delete: :cascade
add_foreign_key "description_versions", "issues", on_delete: :cascade
add_foreign_key "description_versions", "merge_requests", on_delete: :cascade
add_foreign_key "design_management_designs", "issues", on_delete: :cascade
add_foreign_key "design_management_designs", "projects", on_delete: :cascade
add_foreign_key "design_management_designs_versions", "design_management_designs", column: "design_id", name: "fk_03c671965c", on_delete: :cascade
......@@ -4325,6 +4342,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_220135) do
add_foreign_key "software_license_policies", "software_licenses", on_delete: :cascade
add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "suggestions", "notes", on_delete: :cascade
add_foreign_key "system_note_metadata", "description_versions", name: "fk_fbd87415c9", on_delete: :nullify
add_foreign_key "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade
add_foreign_key "term_agreements", "application_setting_terms", column: "term_id"
add_foreign_key "term_agreements", "users", on_delete: :cascade
......
# frozen_string_literal: true
module EE
module DescriptionVersion
extend ActiveSupport::Concern
prepended do
belongs_to :epic
end
class_methods do
def issuable_attrs
(super + %i(epic)).freeze
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe DescriptionVersion do
describe 'associations' do
it { is_expected.to belong_to :epic }
end
describe 'validations' do
it 'is valid when epic_id is set' do
expect(described_class.new(epic_id: 1)).to be_valid
end
end
end
......@@ -794,4 +794,6 @@ describe Epic do
let(:default_params) { {} }
end
end
it_behaves_like 'versioned description'
end
......@@ -25,6 +25,7 @@ issues:
- epic
- designs
- design_versions
- description_versions
- prometheus_alerts
- prometheus_alert_events
- self_managed_prometheus_alert_events
......@@ -132,6 +133,7 @@ merge_requests:
- blocks_as_blockee
- blocking_merge_requests
- blocked_merge_requests
- description_versions
external_pull_requests:
- project
merge_request_diff:
......
# frozen_string_literal: true
require 'spec_helper'
describe DescriptionVersion do
describe 'associations' do
it { is_expected.to belong_to :issue }
it { is_expected.to belong_to :merge_request }
end
describe 'validations' do
describe 'exactly_one_issuable' do
using RSpec::Parameterized::TableSyntax
subject { described_class.new(issue_id: issue_id, merge_request_id: merge_request_id).valid? }
where(:issue_id, :merge_request_id, :valid?) do
nil | 1 | true
1 | nil | true
nil | nil | false
1 | 1 | false
end
with_them do
it { is_expected.to eq(valid?) }
end
end
end
end
......@@ -899,4 +899,6 @@ describe Issue do
let(:default_params) { { project: project } }
end
end
it_behaves_like 'versioned description'
end
......@@ -3331,4 +3331,6 @@ describe MergeRequest do
it { expect(query).to contain_exactly(merge_request1, merge_request2) }
end
it_behaves_like 'versioned description'
end
......@@ -5,6 +5,7 @@ require 'spec_helper'
describe SystemNoteMetadata do
describe 'associations' do
it { is_expected.to belong_to(:note) }
it { is_expected.to belong_to(:description_version) }
end
describe 'validation' do
......
......@@ -46,5 +46,17 @@ describe NoteSummary do
it 'returns metadata hash' do
expect(create_note_summary.metadata).to eq(action: 'icon', commit_count: 5)
end
context 'description action and noteable has saved_description_version' do
before do
noteable.saved_description_version = 1
end
subject { described_class.new(noteable, project, user, 'note', action: 'description') }
it 'sets the description_version metadata' do
expect(subject.metadata).to include(description_version: 1)
end
end
end
end
......@@ -212,6 +212,15 @@ describe ::SystemNotes::IssuablesService do
it 'sets the note text' do
expect(subject.note).to eq('changed the description')
end
it 'associates the related description version' do
noteable.update!(description: 'New description')
description_version_id = subject.system_note_metadata.description_version_id
expect(description_version_id).not_to be_nil
expect(description_version_id).to eq(noteable.saved_description_version.id)
end
end
end
......
# frozen_string_literal: true
RSpec.shared_examples 'versioned description' do
describe 'associations' do
it { is_expected.to have_many(:description_versions) }
end
describe 'save_description_version' do
let(:factory_name) { described_class.name.underscore.to_sym }
let!(:model) { create(factory_name, description: 'Original description') }
context 'when feature is enabled' do
before do
stub_feature_flags(save_description_versions: true)
end
context 'when description was changed' do
before do
model.update!(description: 'New description')
end
it 'saves the old and new description for the first update' do
expect(model.description_versions.first.description).to eq('Original description')
expect(model.description_versions.last.description).to eq('New description')
end
it 'only saves the new description for subsequent updates' do
expect { model.update!(description: 'Another description') }.to change { model.description_versions.count }.by(1)
expect(model.description_versions.last.description).to eq('Another description')
end
it 'sets the new description version to `saved_description_version`' do
expect(model.saved_description_version).to eq(model.description_versions.last)
end
it 'clears `saved_description_version` after another save that does not change description' do
model.save!
expect(model.saved_description_version).to be_nil
end
end
context 'when description was not changed' do
it 'does not save any description version' do
expect { model.save! }.not_to change { model.description_versions.count }
expect(model.saved_description_version).to be_nil
end
end
end
context 'when feature is disabled' do
before do
stub_feature_flags(save_description_versions: false)
end
it 'does not save any description version' do
expect { model.update!(description: 'New description') }.not_to change { model.description_versions.count }
expect(model.saved_description_version).to be_nil
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment