Class: Repositories::ChangelogService

Inherits:
Object
  • Object
show all
Defined in:
app/services/repositories/changelog_service.rb

Overview

A service class for generating a changelog section.

Constant Summary collapse

DEFAULT_TRAILER =
'Changelog'
DEFAULT_FILE =
'CHANGELOG.md'
COMMITS_LIMIT =

The maximum number of commits allowed to fetch in `from` and `to` range.

This value is arbitrarily chosen. Increasing it means more Gitaly calls and more presure on Gitaly services.

This number is 3x of the average number of commits per GitLab releases. Some examples for GitLab's own releases:

  • 13.6.0: 4636 commits

  • 13.5.0: 5912 commits

  • 13.4.0: 5541 commits

15_000

Instance Method Summary collapse

Constructor Details

#initialize(project, user, version:, branch: project.default_branch_or_main, from: nil, to: branch, date: DateTime.now, trailer: DEFAULT_TRAILER, file: DEFAULT_FILE, message: "Add changelog for version #{version}") ⇒ ChangelogService

The `project` specifies the `Project` to generate the changelog section for.

The `user` argument specifies a `User` to use for committing the changes to the Git repository.

The `version` arguments must be a version `String` using semantic versioning as the format.

The arguments `from` and `to` must specify a Git ref or SHA to use for fetching the commits to include in the changelog. The SHA/ref set in the `from` argument isn't included in the list.

The `date` argument specifies the date of the release, and defaults to the current time/date.

The `branch` argument specifies the branch to commit the changes to. The branch must already exist.

The `trailer` argument is the Git trailer to use for determining what commits to include in the changelog.

The `file` arguments specifies the name/path of the file to commit the changes to. If the file doesn't exist, it's created automatically.

The `message` argument specifies the commit message to use when committing the changelog changes.

rubocop: disable Metrics/ParameterLists


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'app/services/repositories/changelog_service.rb', line 51

def initialize(
  project,
  user,
  version:,
  branch: project.default_branch_or_main,
  from: nil,
  to: branch,
  date: DateTime.now,
  trailer: DEFAULT_TRAILER,
  file: DEFAULT_FILE,
  message: "Add changelog for version #{version}"
)
  @project = project
  @user = user
  @version = version
  @from = from
  @to = to
  @date = date
  @branch = branch
  @trailer = trailer
  @file = file
  @message = message
end

Instance Method Details

#execute(commit_to_changelog: true) ⇒ Object

rubocop: enable Metrics/ParameterLists


76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'app/services/repositories/changelog_service.rb', line 76

def execute(commit_to_changelog: true)
  config = Gitlab::Changelog::Config.from_git(@project, @user)
  from = start_of_commit_range(config)

  # 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
  # the number of SQL queries needed to get this data.
  mrs_finder = MergeRequests::OldestPerCommitFinder.new(@project)
  release = Gitlab::Changelog::Release
    .new(version: @version, date: @date, config: config)

  commits =
    ChangelogCommitsFinder.new(project: @project, from: from, to: @to)

  verify_commit_range!(from, @to)

  commits.each_page(@trailer) do |page|
    mrs = mrs_finder.execute(page)

    # Preload the authors. This ensures we only need a single SQL query per
    # batch of commits, instead of needing a query for every commit.
    page.each(&:lazy_author)

    # Preload author permissions
    @project.team.max_member_access_for_user_ids(page.map(&:author).compact.map(&:id))

    page.each do |commit|
      release.add_entry(
        title: commit.title,
        commit: commit,
        category: commit.trailers.fetch(@trailer),
        author: commit.author,
        merge_request: mrs[commit.id]
      )
    end
  end

  if commit_to_changelog
    Gitlab::Changelog::Committer
      .new(@project, @user)
      .commit(release: release, file: @file, branch: @branch, message: @message)
  else
    Gitlab::Changelog::Generator.new.add(release)
  end
end

#start_of_commit_range(config) ⇒ Object


123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'app/services/repositories/changelog_service.rb', line 123

def start_of_commit_range(config)
  return @from if @from

  finder = ChangelogTagFinder.new(@project, regex: config.tag_regex)

  if (prev_tag = finder.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

#verify_commit_range!(from, to) ⇒ Object


139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'app/services/repositories/changelog_service.rb', line 139

def verify_commit_range!(from, to)
  return unless Feature.enabled?(:changelog_commits_limitation, @project)

  commits = @project.repository.commits_by(oids: [from, to])

  raise Gitlab::Changelog::Error, "Invalid or not found commit value in the given range" unless commits.count == 2

  _, commits_count = @project.repository.diverging_commit_count(from, to)

  if commits_count > COMMITS_LIMIT
    raise Gitlab::Changelog::Error, "The commits range exceeds #{COMMITS_LIMIT} elements."
  end
end