Class: Fastlane::Actions::AnalyzeCommitsAction

Inherits:
Action
  • Object
show all
Defined in:
lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb

Documentation collapse

Class Method Summary collapse

Class Method Details

.authorsObject



333
334
335
336
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 333

def self.authors
  # So no one will ever forget your contribution to fastlane :) You are awesome btw!
  ["xotahal"]
end

.available_optionsObject



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 236

def self.available_options
  # Define all options your action supports.

  # Below a few examples
  [
    FastlaneCore::ConfigItem.new(
      key: :match,
      description: "Match parameter of git describe. See man page of git describe for more info",
      verify_block: proc do |value|
        UI.user_error!("No match for analyze_commits action given, pass using `match: 'expr'`") unless value && !value.empty?
      end
    ),
    FastlaneCore::ConfigItem.new(
      key: :commit_format,
      description: "The commit format to apply. Presets are 'default' or 'angular', or you can provide your own Regexp. Note: the supplied regex _must_ have 4 capture groups, in order: type, scope, has_exclamation_mark, and subject",
      default_value: "default",
      is_string: false,
      verify_block: proc do |value|
        case value
        when String
          unless Helper::SemanticConventionReleaseHelper.format_patterns.key?(value)
            UI.user_error!("Invalid format preset: #{value}")
          end

          pattern = Helper::SemanticConventionReleaseHelper.format_patterns[value]
        when Regexp
          pattern = value
        else
          UI.user_error!("Invalid option type: #{value.inspect}")
        end
        Actions.lane_context[SharedValues::CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN] = pattern
      end
    ),
    FastlaneCore::ConfigItem.new(
      key: :releases,
      description: "Map types of commit to release (major, minor, patch)",
      default_value: { fix: "patch", feat: "minor", BREAKING_CHANGE: "major" },
      type: Hash
    ),
    FastlaneCore::ConfigItem.new(
      key: :codepush_friendly,
      description: "These types are consider as codepush friendly automatically",
      default_value: ["chore", "test", "docs", "style", "refactor", "perf"],
      type: Array,
      optional: true
    ),
    FastlaneCore::ConfigItem.new(
      key: :tag_version_match,
      description: "To parse version number from tag name",
      default_value: '\d+\.\d+\.\d+'
    ),
    FastlaneCore::ConfigItem.new(
      key: :ignore_scopes,
      description: "To ignore certain scopes when calculating releases",
      default_value: ["skip"],
      type: Array,
      optional: true
    ),
    FastlaneCore::ConfigItem.new(
      key: :show_version_path,
      description: "True if you want to print out the version calculated for each commit",
      default_value: true,
      type: Boolean,
      optional: true
    ),
    FastlaneCore::ConfigItem.new(
      key: :debug,
      description: "True if you want to log out a debug info",
      default_value: false,
      type: Boolean,
      optional: true
    )
  ]
end

.descriptionObject



228
229
230
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 228

def self.description
  "Finds a tag of last release and determinates version of next release"
end

.detailsObject



232
233
234
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 232

def self.details
  "This action will find a last release tag and analyze all commits since the tag. It uses conventional commits. Every time when commit is marked as fix or feat it will increase patch or minor number (you can setup this default behaviour). After all it will suggest if the version should be released or not."
end

.get_commits_from_hash(params) ⇒ Object



34
35
36
37
38
39
40
41
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 34

def self.get_commits_from_hash(params)
  commits = Helper::SemanticConventionReleaseHelper.git_log(
    pretty: '%s|%b|>',
    start: params[:hash],
    debug: params[:debug]
  )
  commits.split("|>")
end

.get_last_tag(params) ⇒ Object



20
21
22
23
24
25
26
27
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 20

def self.get_last_tag(params)
  # Try to find the tag
  command = "git describe --tags --match=#{params[:match]}"
  Actions.sh(command, log: params[:debug])
rescue
  UI.message("Tag was not found for match pattern - #{params[:match]}")
  ''
end

.get_last_tag_hash(params) ⇒ Object



29
30
31
32
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 29

def self.get_last_tag_hash(params)
  command = "git rev-list -n 1 refs/tags/#{params[:tag_name]}"
  Actions.sh(command, log: params[:debug]).chomp
end

.is_codepush_friendly(params) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 156

def self.is_codepush_friendly(params)
  git_command = 'git rev-list --max-parents=0 HEAD'
  # Begining of the branch is taken for codepush analysis
  hash_lines = Actions.sh("#{git_command} | wc -l", log: params[:debug]).chomp
  hash = Actions.sh(git_command, log: params[:debug]).chomp
  next_major = 0
  next_minor = 0
  next_patch = 0
  last_incompatible_codepush_version = '0.0.0'

  if hash_lines.to_i > 1
    UI.error("#{git_command} resulted to more than 1 hash")
    UI.error('This usualy happens when you pull only part of a git history. Check out how you pull the repo! "git fetch" should be enough.')
    Actions.sh(git_command, log: true).chomp
    return false
  end

  # Get commits log between last version and head
  splitted = get_commits_from_hash(
    hash: hash,
    debug: params[:debug]
  )
  releases = params[:releases]
  codepush_friendly = params[:codepush_friendly]
  patch_updated = false
  minor_updated = false
  major_updated = false
  format_pattern = lane_context[SharedValues::CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN]
  splitted.each do |line|
    # conventional commits are in format
    # type: subject (fix: app crash - for example)
    commit = Helper::SemanticConventionReleaseHelper.parse_commit(
      commit_subject: line.split("|")[0],
      commit_body: line.split("|")[1],
      releases: releases,
      pattern: format_pattern,
      codepush_friendly: codepush_friendly
    )

    if (commit[:release] == "major" || commit[:is_breaking_change]) && !major_updated
      next_major += 1
      next_minor = 0
      next_patch = 0
      major_updated = true
    elsif commit[:release] == "minor" && !major_updated && !minor_updated
      next_minor += 1
      next_patch = 0
      minor_updated = true
    elsif commit[:release] == "patch" && !major_updated && !minor_updated && !patch_updated
      next_patch += 1
      patch_updated = true
    end

    unless commit[:is_codepush_friendly]
      last_incompatible_codepush_version = "#{next_major}.#{next_minor}.#{next_patch}"
    end
  end

  Actions.lane_context[SharedValues::RELEASE_LAST_INCOMPATIBLE_CODEPUSH_VERSION] = last_incompatible_codepush_version
end

.is_releasable(params) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 43

def self.is_releasable(params)
  # Hash of the commit where is the last version
  # If the tag is not found we are taking HEAD as reference
  hash = 'HEAD'
  # Default last version
  version = '0.0.0'

  tag = get_last_tag(
    match: params[:match],
    debug: params[:debug]
  )

  if tag.empty?
    UI.message("First commit of the branch is taken as a begining of next release")
    # If there is no tag found we taking the first commit of current branch
    hash = Actions.sh('git rev-list --max-parents=0 HEAD', log: params[:debug]).chomp
  else
    # Tag's format is v2.3.4-5-g7685948
    # See git describe man page for more info
    tag_name = tag.split('-')[0...-2].join('-').strip
    parsed_version = tag_name.match(params[:tag_version_match])

    if parsed_version.nil?
      UI.user_error!("Error while parsing version from tag #{tag_name} by using tag_version_match - #{params[:tag_version_match]}. Please check if the tag contains version as you expect and if you are using single brackets for tag_version_match parameter.")
    end

    version = parsed_version[0]
    # Get a hash of last version tag
    hash = get_last_tag_hash(
      tag_name: tag_name,
      debug: params[:debug]
    )

    UI.message("Found a tag #{tag_name} associated with version #{version}")
  end

  # converts last version string to the int numbers
  next_major = (version.split('.')[0] || 0).to_i
  next_minor = (version.split('.')[1] || 0).to_i
  next_patch = (version.split('.')[2] || 0).to_i

  # Get commits log between last version and head
  splitted = get_commits_from_hash(
    hash: hash,
    debug: params[:debug]
  )

  UI.message("Found #{splitted.length} commits since last release")
  releases = params[:releases]

  format_pattern = lane_context[SharedValues::CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN]

  patch_updated = false
  minor_updated = false
  major_updated = false
  splitted.each do |line|
    parts = line.split("|")
    subject = parts[0].strip
    # conventional commits are in format
    # type: subject (fix: app crash - for example)
    commit = Helper::SemanticConventionReleaseHelper.parse_commit(
      commit_subject: subject,
      commit_body: parts[1],
      releases: releases,
      pattern: format_pattern
    )

    unless commit[:scope].nil?
      # if this commit has a scope, then we need to inspect to see if that is one of the scopes we're trying to exclude
      scope = commit[:scope]
      scopes_to_ignore = params[:ignore_scopes]
      # if it is, we'll skip this commit when bumping versions
      next if scopes_to_ignore.include?(scope) #=> true
    end

    if (commit[:release] == "major" || commit[:is_breaking_change]) && !major_updated
      next_major += 1
      next_minor = 0
      next_patch = 0
      major_updated = true
    elsif commit[:release] == "minor" && !major_updated && !minor_updated
      next_minor += 1
      next_patch = 0
      minor_updated = minor_updated
    elsif commit[:release] == "patch" && !major_updated && !minor_updated && !patch_updated
      next_patch += 1
      patch_updated = true
    end

    next_version = "#{next_major}.#{next_minor}.#{next_patch}"
    UI.message("#{next_version}: #{subject}") if params[:show_version_path]
  end

  next_version = "#{next_major}.#{next_minor}.#{next_patch}"
  is_next_version_releasable = Helper::SemanticConventionReleaseHelper.semver_gt(next_version, version)

  Actions.lane_context[SharedValues::RELEASE_ANALYZED] = true
  Actions.lane_context[SharedValues::RELEASE_IS_NEXT_VERSION_HIGHER] = is_next_version_releasable
  # Last release analysis
  Actions.lane_context[SharedValues::RELEASE_LAST_TAG_HASH] = hash
  Actions.lane_context[SharedValues::RELEASE_LAST_VERSION] = version
  # Next release analysis
  Actions.lane_context[SharedValues::RELEASE_NEXT_MAJOR_VERSION] = next_major
  Actions.lane_context[SharedValues::RELEASE_NEXT_MINOR_VERSION] = next_minor
  Actions.lane_context[SharedValues::RELEASE_NEXT_PATCH_VERSION] = next_patch
  Actions.lane_context[SharedValues::RELEASE_NEXT_VERSION] = next_version

  success_message = "Next version (#{next_version}) is higher than last version (#{version}). This version should be released."
  UI.success(success_message) if is_next_version_releasable

  is_next_version_releasable
end

.is_supported?(platform) ⇒ Boolean

Returns:

  • (Boolean)


338
339
340
341
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 338

def self.is_supported?(platform)
  # you can do things like
  true
end

.outputObject



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 311

def self.output
  # Define the shared values you are going to provide
  # Example
  [
    ['RELEASE_ANALYZED', 'True if commits were analyzed.'],
    ['RELEASE_IS_NEXT_VERSION_HIGHER', 'True if next version is higher then last version'],
    ['RELEASE_LAST_TAG_HASH', 'Hash of commit that is tagged as a last version'],
    ['RELEASE_LAST_VERSION', 'Last version number - parsed from last tag.'],
    ['RELEASE_NEXT_MAJOR_VERSION', 'Major number of the next version'],
    ['RELEASE_NEXT_MINOR_VERSION', 'Minor number of the next version'],
    ['RELEASE_NEXT_PATCH_VERSION', 'Patch number of the next version'],
    ['RELEASE_NEXT_VERSION', 'Next version string in format (major.minor.patch)'],
    ['RELEASE_LAST_INCOMPATIBLE_CODEPUSH_VERSION', 'Last commit without codepush'],
    ['CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN', 'The format pattern Regexp used to match commits (mainly for internal use)']
  ]
end

.return_valueObject



328
329
330
331
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 328

def self.return_value
  # If your method provides a return value, you can describe here what it does
  "Returns true if the next version is higher then the last version"
end

.run(params) ⇒ Object



217
218
219
220
221
222
# File 'lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb', line 217

def self.run(params)
  is_next_version_releasable = is_releasable(params)
  is_codepush_friendly(params)

  is_next_version_releasable
end