2021-03-11 19:13:27 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require 'spec_helper'
|
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
RSpec.describe Repositories::ChangelogService, feature_category: :source_code_management do
|
2021-03-11 19:13:27 +05:30
|
|
|
describe '#execute' do
|
2021-04-17 20:07:23 +05:30
|
|
|
let!(:project) { create(:project, :empty_repo) }
|
|
|
|
let!(:creator) { project.creator }
|
|
|
|
let!(:author1) { create(:user) }
|
|
|
|
let!(:author2) { create(:user) }
|
|
|
|
let!(:mr1) { create(:merge_request, :merged, target_project: project) }
|
|
|
|
let!(:mr2) { create(:merge_request, :merged, target_project: project) }
|
|
|
|
|
|
|
|
# The range of commits ignores the first commit, but includes the last
|
|
|
|
# commit. To ensure both the commits below are included, we must create an
|
|
|
|
# extra commit.
|
|
|
|
#
|
|
|
|
# In the real world, the start commit of the range will be the last commit
|
|
|
|
# of the previous release, so ignoring that is expected and desired.
|
|
|
|
let!(:sha1) do
|
|
|
|
create_commit(
|
2021-03-11 19:13:27 +05:30
|
|
|
project,
|
|
|
|
creator,
|
|
|
|
commit_message: 'Initial commit',
|
|
|
|
actions: [{ action: 'create', content: 'test', file_path: 'README.md' }]
|
|
|
|
)
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
let!(:sha2) do
|
|
|
|
project.add_maintainer(author1)
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
create_commit(
|
2021-03-11 19:13:27 +05:30
|
|
|
project,
|
|
|
|
author1,
|
|
|
|
commit_message: "Title 1\n\nChangelog: feature",
|
|
|
|
actions: [{ action: 'create', content: 'foo', file_path: 'a.txt' }]
|
|
|
|
)
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
let!(:sha3) do
|
|
|
|
project.add_maintainer(author2)
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
create_commit(
|
2021-03-11 19:13:27 +05:30
|
|
|
project,
|
|
|
|
author2,
|
|
|
|
commit_message: "Title 2\n\nChangelog: feature",
|
|
|
|
actions: [{ action: 'create', content: 'bar', file_path: 'b.txt' }]
|
|
|
|
)
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
let!(:sha4) do
|
|
|
|
create_commit(
|
|
|
|
project,
|
|
|
|
author2,
|
|
|
|
commit_message: "Title 3\n\nChangelog: feature",
|
|
|
|
actions: [{ action: 'create', content: 'bar', file_path: 'c.txt' }]
|
|
|
|
)
|
|
|
|
end
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
let!(:commit1) { project.commit(sha2) }
|
|
|
|
let!(:commit2) { project.commit(sha3) }
|
|
|
|
let!(:commit3) { project.commit(sha4) }
|
|
|
|
|
2022-01-26 12:08:38 +05:30
|
|
|
let(:commit_to_changelog) { true }
|
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
it 'generates and commits a changelog section' do
|
2021-03-11 19:13:27 +05:30
|
|
|
allow(MergeRequestDiffCommit)
|
|
|
|
.to receive(:oldest_merge_request_id_per_commit)
|
|
|
|
.with(project.id, [commit2.id, commit1.id])
|
2022-11-25 23:54:43 +05:30
|
|
|
.and_return(
|
|
|
|
[
|
|
|
|
{ sha: sha2, merge_request_id: mr1.id },
|
|
|
|
{ sha: sha3, merge_request_id: mr2.id }
|
|
|
|
])
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
service = described_class
|
|
|
|
.new(project, creator, version: '1.0.0', from: sha1, to: sha3)
|
|
|
|
|
2022-01-26 12:08:38 +05:30
|
|
|
recorder = ActiveRecord::QueryRecorder.new { service.execute(commit_to_changelog: commit_to_changelog) }
|
2021-04-17 20:07:23 +05:30
|
|
|
changelog = project.repository.blob_at('master', 'CHANGELOG.md')&.data
|
|
|
|
|
2023-03-17 16:20:25 +05:30
|
|
|
expect(recorder.count).to eq(12)
|
2021-04-17 20:07:23 +05:30
|
|
|
expect(changelog).to include('Title 1', 'Title 2')
|
|
|
|
end
|
|
|
|
|
|
|
|
it "ignores a commit when it's both added and reverted in the same range" do
|
|
|
|
create_commit(
|
|
|
|
project,
|
|
|
|
author2,
|
|
|
|
commit_message: "Title 4\n\nThis reverts commit #{sha4}",
|
|
|
|
actions: [{ action: 'create', content: 'bar', file_path: 'd.txt' }]
|
|
|
|
)
|
|
|
|
|
|
|
|
described_class
|
|
|
|
.new(project, creator, version: '1.0.0', from: sha1)
|
2022-01-26 12:08:38 +05:30
|
|
|
.execute(commit_to_changelog: commit_to_changelog)
|
2021-03-11 19:13:27 +05:30
|
|
|
|
|
|
|
changelog = project.repository.blob_at('master', 'CHANGELOG.md')&.data
|
|
|
|
|
|
|
|
expect(changelog).to include('Title 1', 'Title 2')
|
2021-04-17 20:07:23 +05:30
|
|
|
expect(changelog).not_to include('Title 3', 'Title 4')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'includes a revert commit when it has a trailer' do
|
|
|
|
create_commit(
|
|
|
|
project,
|
|
|
|
author2,
|
|
|
|
commit_message: "Title 4\n\nThis reverts commit #{sha4}\n\nChangelog: added",
|
|
|
|
actions: [{ action: 'create', content: 'bar', file_path: 'd.txt' }]
|
|
|
|
)
|
|
|
|
|
|
|
|
described_class
|
|
|
|
.new(project, creator, version: '1.0.0', from: sha1)
|
2022-01-26 12:08:38 +05:30
|
|
|
.execute(commit_to_changelog: commit_to_changelog)
|
2021-04-17 20:07:23 +05:30
|
|
|
|
|
|
|
changelog = project.repository.blob_at('master', 'CHANGELOG.md')&.data
|
|
|
|
|
|
|
|
expect(changelog).to include('Title 1', 'Title 2', 'Title 4')
|
|
|
|
expect(changelog).not_to include('Title 3')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'uses the target branch when "to" is unspecified' do
|
|
|
|
described_class
|
|
|
|
.new(project, creator, version: '1.0.0', from: sha1)
|
2022-01-26 12:08:38 +05:30
|
|
|
.execute(commit_to_changelog: commit_to_changelog)
|
2021-04-17 20:07:23 +05:30
|
|
|
|
|
|
|
changelog = project.repository.blob_at('master', 'CHANGELOG.md')&.data
|
|
|
|
|
|
|
|
expect(changelog).to include('Title 1', 'Title 2', 'Title 3')
|
2021-03-11 19:13:27 +05:30
|
|
|
end
|
2022-01-26 12:08:38 +05:30
|
|
|
|
|
|
|
describe 'with commit_to_changelog: false' do
|
|
|
|
let(:commit_to_changelog) { false }
|
|
|
|
|
|
|
|
it 'generates changelog section' do
|
|
|
|
allow(MergeRequestDiffCommit)
|
|
|
|
.to receive(:oldest_merge_request_id_per_commit)
|
|
|
|
.with(project.id, [commit2.id, commit1.id])
|
2022-11-25 23:54:43 +05:30
|
|
|
.and_return(
|
|
|
|
[
|
|
|
|
{ sha: sha2, merge_request_id: mr1.id },
|
|
|
|
{ sha: sha3, merge_request_id: mr2.id }
|
|
|
|
])
|
2022-01-26 12:08:38 +05:30
|
|
|
|
|
|
|
service = described_class
|
|
|
|
.new(project, creator, version: '1.0.0', from: sha1, to: sha3)
|
|
|
|
|
|
|
|
changelog = service.execute(commit_to_changelog: commit_to_changelog)
|
|
|
|
|
|
|
|
expect(changelog).to include('Title 1', 'Title 2')
|
|
|
|
end
|
|
|
|
end
|
2022-07-23 23:45:48 +05:30
|
|
|
|
|
|
|
it 'avoids N+1 queries', :request_store do
|
|
|
|
RequestStore.clear!
|
|
|
|
|
|
|
|
request = ->(to) do
|
|
|
|
described_class
|
|
|
|
.new(project, creator, version: '1.0.0', from: sha1, to: to)
|
|
|
|
.execute(commit_to_changelog: false)
|
|
|
|
end
|
|
|
|
|
|
|
|
control = ActiveRecord::QueryRecorder.new { request.call(sha2) }
|
|
|
|
|
|
|
|
RequestStore.clear!
|
|
|
|
|
|
|
|
expect { request.call(sha3) }.not_to exceed_query_limit(control.count)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when one of commits does not exist' do
|
|
|
|
let(:service) { described_class.new(project, creator, version: '1.0.0', from: 'master', to: '54321') }
|
|
|
|
|
|
|
|
it 'raises an exception' do
|
|
|
|
expect { service.execute(commit_to_changelog: false) }.to raise_error(Gitlab::Changelog::Error)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when commit range exceeds the limit' do
|
|
|
|
let(:service) { described_class.new(project, creator, version: '1.0.0', from: sha1) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
stub_const("#{described_class.name}::COMMITS_LIMIT", 2)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'raises an exception' do
|
|
|
|
expect { service.execute(commit_to_changelog: false) }.to raise_error(Gitlab::Changelog::Error)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when feature flag is off' do
|
|
|
|
before do
|
|
|
|
stub_feature_flags(changelog_commits_limitation: false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns the changelog' do
|
|
|
|
expect(service.execute(commit_to_changelog: false)).to include('Title 1', 'Title 2', 'Title 3')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2022-08-13 15:12:31 +05:30
|
|
|
|
|
|
|
context 'with specified changelog config file path' do
|
|
|
|
it 'return specified changelog content' do
|
|
|
|
config = Gitlab::Changelog::Config.from_hash(project, { 'template' => 'specified_changelog_content' }, creator)
|
|
|
|
|
|
|
|
allow(Gitlab::Changelog::Config)
|
|
|
|
.to receive(:from_git)
|
|
|
|
.with(project, creator, 'specified_changelog_config.yml')
|
|
|
|
.and_return(config)
|
|
|
|
|
|
|
|
described_class
|
|
|
|
.new(project, creator, version: '1.0.0', from: sha1, config_file: 'specified_changelog_config.yml')
|
|
|
|
.execute(commit_to_changelog: commit_to_changelog)
|
|
|
|
|
|
|
|
changelog = project.repository.blob_at('master', 'CHANGELOG.md')&.data
|
|
|
|
|
|
|
|
expect(changelog).to include('specified_changelog_content')
|
|
|
|
end
|
|
|
|
end
|
2021-03-11 19:13:27 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
describe '#start_of_commit_range' do
|
|
|
|
let(:project) { build_stubbed(:project) }
|
|
|
|
let(:user) { build_stubbed(:user) }
|
2021-04-29 21:17:54 +05:30
|
|
|
let(:config) { Gitlab::Changelog::Config.new(project) }
|
2021-03-11 19:13:27 +05:30
|
|
|
|
|
|
|
context 'when the "from" argument is specified' do
|
|
|
|
it 'returns the value of the argument' do
|
|
|
|
service = described_class
|
|
|
|
.new(project, user, version: '1.0.0', from: 'foo', to: 'bar')
|
|
|
|
|
2021-04-29 21:17:54 +05:30
|
|
|
expect(service.start_of_commit_range(config)).to eq('foo')
|
2021-03-11 19:13:27 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the "from" argument is unspecified' do
|
|
|
|
it 'returns the tag commit of the previous version' do
|
|
|
|
service = described_class
|
|
|
|
.new(project, user, version: '1.0.0', to: 'bar')
|
|
|
|
|
2021-04-29 21:17:54 +05:30
|
|
|
finder_spy = instance_spy(Repositories::ChangelogTagFinder)
|
2021-03-11 19:13:27 +05:30
|
|
|
tag = double(:tag, target_commit: double(:commit, id: '123'))
|
|
|
|
|
2021-04-29 21:17:54 +05:30
|
|
|
allow(Repositories::ChangelogTagFinder)
|
2021-03-11 19:13:27 +05:30
|
|
|
.to receive(:new)
|
2021-04-29 21:17:54 +05:30
|
|
|
.with(project, regex: an_instance_of(String))
|
2021-03-11 19:13:27 +05:30
|
|
|
.and_return(finder_spy)
|
|
|
|
|
|
|
|
allow(finder_spy)
|
|
|
|
.to receive(:execute)
|
|
|
|
.with('1.0.0')
|
|
|
|
.and_return(tag)
|
|
|
|
|
2021-04-29 21:17:54 +05:30
|
|
|
expect(service.start_of_commit_range(config)).to eq('123')
|
2021-03-11 19:13:27 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'raises an error when no tag is found' do
|
|
|
|
service = described_class
|
|
|
|
.new(project, user, version: '1.0.0', to: 'bar')
|
|
|
|
|
2021-04-29 21:17:54 +05:30
|
|
|
finder_spy = instance_spy(Repositories::ChangelogTagFinder)
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2021-04-29 21:17:54 +05:30
|
|
|
allow(Repositories::ChangelogTagFinder)
|
2021-03-11 19:13:27 +05:30
|
|
|
.to receive(:new)
|
2021-04-29 21:17:54 +05:30
|
|
|
.with(project, regex: an_instance_of(String))
|
2021-03-11 19:13:27 +05:30
|
|
|
.and_return(finder_spy)
|
|
|
|
|
|
|
|
allow(finder_spy)
|
|
|
|
.to receive(:execute)
|
|
|
|
.with('1.0.0')
|
|
|
|
.and_return(nil)
|
|
|
|
|
2021-04-29 21:17:54 +05:30
|
|
|
expect { service.start_of_commit_range(config) }
|
2021-03-11 19:13:27 +05:30
|
|
|
.to raise_error(Gitlab::Changelog::Error)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def create_commit(project, user, params)
|
|
|
|
params = { start_branch: 'master', branch_name: 'master' }.merge(params)
|
|
|
|
Files::MultiService.new(project, user, params).execute.fetch(:result)
|
|
|
|
end
|
|
|
|
end
|