Commit f3bb5b3b authored by Jan Provaznik's avatar Jan Provaznik

Merge branch 'changelog-from-optional' into 'master'

Make `from` optional in the changelog API

See merge request gitlab-org/gitlab!53736
parents dd53c00c 6ecee34c
# frozen_string_literal: true
module Repositories
# A finder class for getting the tag of the last release before a given
# version.
#
# Imagine a project with the following tags:
#
# * v1.0.0
# * v1.1.0
# * v2.0.0
#
# If the version supplied is 2.1.0, the tag returned will be v2.0.0. And when
# the version is 1.1.1, or 1.2.0, the returned tag will be v1.1.0.
#
# This finder expects that all tags to consider meet the following
# requirements:
#
# * They start with the letter "v"
# * They use semantic versioning for the tag format
#
# Tags not meeting these requirements are ignored.
class PreviousTagFinder
TAG_REGEX = /\Av(?<version>#{Gitlab::Regex.unbounded_semver_regex})\z/.freeze
def initialize(project)
@project = project
end
def execute(new_version)
tags = {}
versions = [new_version]
@project.repository.tags.each do |tag|
matches = tag.name.match(TAG_REGEX)
next unless matches
version = matches[:version]
tags[version] = tag
versions << version
end
VersionSorter.sort!(versions)
index = versions.index(new_version)
tags[versions[index - 1]] if index&.positive?
end
end
end
......@@ -39,8 +39,8 @@ module Repositories
project,
user,
version:,
from:,
to:,
from: nil,
date: DateTime.now,
branch: project.default_branch_or_master,
trailer: DEFAULT_TRAILER,
......@@ -61,6 +61,8 @@ module Repositories
# rubocop: enable Metrics/ParameterLists
def execute
from = start_of_commit_range
# For every entry we want to only include the merge request that
# originally introduced the commit, which is the oldest merge request that
# contains the commit. We fetch there merge requests in batches, reducing
......@@ -71,7 +73,7 @@ module Repositories
.new(version: @version, date: @date, config: config)
commits =
CommitsWithTrailerFinder.new(project: @project, from: @from, to: @to)
CommitsWithTrailerFinder.new(project: @project, from: from, to: @to)
commits.each_page(@trailer) do |page|
mrs = mrs_finder.execute(page)
......@@ -95,5 +97,19 @@ module Repositories
.new(@project, @user)
.commit(release: release, file: @file, branch: @branch, message: @message)
end
def start_of_commit_range
return @from if @from
if (prev_tag = PreviousTagFinder.new(@project).execute(@version))
return prev_tag.target_commit.id
end
raise(
Gitlab::Changelog::Error,
'The commit start range is unspecified, and no previous tag ' \
'could be found to use instead'
)
end
end
end
......@@ -309,7 +309,7 @@ Supported attributes:
| Attribute | Type | Required | Description |
| :-------- | :------- | :--------- | :---------- |
| `version` | string | yes | The version to generate the changelog for. The format must follow [semantic versioning](https://semver.org/). |
| `from` | string | yes | The start of the range of commits (as a SHA) to use for generating the changelog. This commit itself isn't included in the list. |
| `from` | string | no | The start of the range of commits (as a SHA) to use for generating the changelog. This commit itself isn't included in the list. |
| `to` | string | yes | The end of the range of commits (as a SHA) to use for the changelog. This commit _is_ included in the list. |
| `date` | datetime | no | The date and time of the release, defaults to the current time. |
| `branch` | string | no | The branch to commit the changelog changes to, defaults to the project's default branch. |
......@@ -317,6 +317,29 @@ Supported attributes:
| `file` | string | no | The file to commit the changes to, defaults to `CHANGELOG.md`. |
| `message` | string | no | The commit message to produce when committing the changes, defaults to `Add changelog for version X` where X is the value of the `version` argument. |
If the `from` attribute is unspecified, GitLab uses the Git tag of the last
version that came before the version specified in the `version` attribute. For
this to work, your project must create Git tags for versions using the
following format:
```plaintext
vX.Y.Z
```
Where `X.Y.Z` is a version that follows semantic versioning. For example,
consider a project with the following tags:
- v1.0.0
- v1.1.0
- v2.0.0
If the `version` attribute is `2.1.0`, GitLab uses tag v2.0.0. And when the
version is `1.1.1`, or `1.2.0`, GitLab uses tag v1.1.0.
If `from` is unspecified and no tag to use is found, the API produces an error.
To solve such an error, you must explicitly specify a value for the `from`
attribute.
### How it works
Changelogs are generated based on commit titles. Commits are only included if
......
......@@ -180,7 +180,7 @@ module API
regexp: Gitlab::Regex.unbounded_semver_regex,
desc: 'The version of the release, using the semantic versioning format'
requires :from,
optional :from,
type: String,
desc: 'The first commit in the range of commits to use for the changelog'
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Repositories::PreviousTagFinder do
let(:project) { build_stubbed(:project) }
let(:finder) { described_class.new(project) }
describe '#execute' do
context 'when there is a previous tag' do
it 'returns the previous tag' do
tag1 = double(:tag1, name: 'v1.0.0')
tag2 = double(:tag2, name: 'v1.1.0')
tag3 = double(:tag3, name: 'v2.0.0')
tag4 = double(:tag4, name: '1.0.0')
allow(project.repository)
.to receive(:tags)
.and_return([tag1, tag3, tag2, tag4])
expect(finder.execute('2.1.0')).to eq(tag3)
expect(finder.execute('2.0.0')).to eq(tag2)
expect(finder.execute('1.5.0')).to eq(tag2)
expect(finder.execute('1.0.1')).to eq(tag1)
end
end
context 'when there is no previous tag' do
it 'returns nil' do
tag1 = double(:tag1, name: 'v1.0.0')
tag2 = double(:tag2, name: 'v1.1.0')
allow(project.repository)
.to receive(:tags)
.and_return([tag1, tag2])
expect(finder.execute('1.0.0')).to be_nil
end
end
end
end
......@@ -67,6 +67,62 @@ RSpec.describe Repositories::ChangelogService do
end
end
describe '#start_of_commit_range' do
let(:project) { build_stubbed(:project) }
let(:user) { build_stubbed(:user) }
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')
expect(service.start_of_commit_range).to eq('foo')
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')
finder_spy = instance_spy(Repositories::PreviousTagFinder)
tag = double(:tag, target_commit: double(:commit, id: '123'))
allow(Repositories::PreviousTagFinder)
.to receive(:new)
.with(project)
.and_return(finder_spy)
allow(finder_spy)
.to receive(:execute)
.with('1.0.0')
.and_return(tag)
expect(service.start_of_commit_range).to eq('123')
end
it 'raises an error when no tag is found' do
service = described_class
.new(project, user, version: '1.0.0', to: 'bar')
finder_spy = instance_spy(Repositories::PreviousTagFinder)
allow(Repositories::PreviousTagFinder)
.to receive(:new)
.with(project)
.and_return(finder_spy)
allow(finder_spy)
.to receive(:execute)
.with('1.0.0')
.and_return(nil)
expect { service.start_of_commit_range }
.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)
......
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