Commit 6ecee34c authored by Yorick Peterse's avatar Yorick Peterse

Make `from` optional in the changelog API

The `from` argument in the changelog generation API is now optional.
When unspecified, GitLab will use the tag of the release that came
before the new release. This means that most users never have to specify
the `from` argument, making it easier to generate changelogs for a new
version.

This fixes
https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1542.
parent ab9552f6
# 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