Class: Fastlane::Actions::AnalyzeCommitsAction

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

Documentation collapse

Class Method Summary collapse

Class Method Details

.authorsObject



383
384
385
386
# File 'lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb', line 383

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

.available_optionsObject



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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb', line 271

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::SemanticReleaseHelper.format_patterns.key?(value)
            UI.user_error!("Invalid format preset: #{value}")
          end

          pattern = Helper::SemanticReleaseHelper.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" },
      type: Hash
    ),
    FastlaneCore::ConfigItem.new(
      key: :codepush_friendly,
      description: "These types are consider as codepush friendly automatically",
      default_value: ["chore", "test", "docs"],
      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: :prevent_tag_fallback,
      description: "Prevent tag from falling back to vX.Y.Z when there is no match",
      default_value: false,
      type: Boolean,
      optional: true
    ),
    FastlaneCore::ConfigItem.new(
      key: :include_scopes,
      description: "To only include certain scopes when calculating releases",
      default_value: [],
      type: Array,
      optional: true
    ),
    FastlaneCore::ConfigItem.new(
      key: :ignore_scopes,
      description: "To ignore certain scopes when calculating releases",
      default_value: [],
      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



263
264
265
# File 'lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb', line 263

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

.detailsObject



267
268
269
# File 'lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb', line 267

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_beginning_of_next_sprint(params) ⇒ Object



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
# File 'lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb', line 44

def self.get_beginning_of_next_sprint(params)
  # command to get first commit
  git_command = "git rev-list --max-parents=0 HEAD"

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

  # if tag doesn't exist it get's first commit or fallback tag (v*.*.*)
  if tag.empty?
    UI.message("It couldn't match tag for #{params[:match]}. Check if first commit can be taken as a beginning of next release")
    # If there is no tag found we taking the first commit of current branch
    hash_lines = Actions.sh("#{git_command} | wc -l", log: params[:debug]).chomp

    if hash_lines.to_i == 1
      UI.message("First commit of the branch is taken as a begining of next release")
      return {
        # here we know this command will return 1 line
        hash: Actions.sh(git_command, log: params[:debug]).chomp
      }
    end

    unless params[:prevent_tag_fallback]
      # neither matched tag and first hash could be used - as fallback we try vX.Y.Z
      UI.message("It couldn't match tag for #{params[:match]} and couldn't use first commit. Check if tag vX.Y.Z can be taken as a begining of next release")
      tag = get_last_tag(match: "v*", debug: params[:debug])
    end

    # even fallback tag doesn't work
    if tag.empty?
      return false
    end
  end

  # Tag's format is v2.3.4-5-g7685948
  # See git describe man page for more info
  # It can be also v2.3.4-5 if there is no commit after tag
  tag_name = tag
  if tag.split('-').length >= 3
    tag_name = tag.split('-')[0...-2].join('-').strip
  end
  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}")

  return {
    hash: hash,
    version: version
  }
end

.get_commits_from_hash(params) ⇒ Object



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

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

.get_last_tag(params) ⇒ Object



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

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



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

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



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb', line 196

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]

  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::SemanticReleaseHelper.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]
      next_major += 1
      next_minor = 0
      next_patch = 0
    elsif commit[:release] == "minor"
      next_minor += 1
      next_patch = 0
    elsif commit[:release] == "patch"
      next_patch += 1
    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



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
155
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
# File 'lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb', line 104

def self.is_releasable(params)
  # Hash of the commit where is the last version
  beginning = get_beginning_of_next_sprint(params)

  unless beginning
    UI.error('It could not find a begining of this sprint. How to fix this:')
    UI.error('-- ensure there is only one commit with --max-parents=0 (this command should return one line: "git rev-list --max-parents=0 HEAD")')
    UI.error('-- tell us explicitely where the release starts by adding tag like this: vX.Y.Z (where X.Y.Z is version from which it starts computing next version number)')
    return false
  end

  # Default last version
  version = beginning[:version] || '0.0.0'
  # If the tag is not found we are taking HEAD as reference
  hash = beginning[:hash] || 'HEAD'

  # 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

  is_next_version_compatible_with_codepush = true

  # 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]
  splitted.each do |line|
    parts = line.split("|")
    subject = parts[0].to_s.strip
    # conventional commits are in format
    # type: subject (fix: app crash - for example)
    commit = Helper::SemanticReleaseHelper.parse_commit(
      commit_subject: subject,
      commit_body: parts[1],
      releases: releases,
      pattern: format_pattern
    )

    next if Helper::SemanticReleaseHelper.should_exclude_commit(
      commit_scope: commit[:scope],
      include_scopes: params[:include_scopes],
      ignore_scopes: params[:ignore_scopes]
    )

    if commit[:release] == "major" || commit[:is_breaking_change]
      next_major += 1
      next_minor = 0
      next_patch = 0
    elsif commit[:release] == "minor"
      next_minor += 1
      next_patch = 0
    elsif commit[:release] == "patch"
      next_patch += 1
    end

    unless commit[:is_codepush_friendly]
      is_next_version_compatible_with_codepush = false
    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::SemanticReleaseHelper.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
  Actions.lane_context[SharedValues::RELEASE_IS_NEXT_VERSION_COMPATIBLE_WITH_CODEPUSH] = is_next_version_compatible_with_codepush
  # 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)


388
389
390
391
# File 'lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb', line 388

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

.outputObject



360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb', line 360

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_IS_NEXT_VERSION_COMPATIBLE_WITH_CODEPUSH', 'True if next version is compatible with codepush'],
    ['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



378
379
380
381
# File 'lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb', line 378

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



252
253
254
255
256
257
# File 'lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb', line 252

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

  is_next_version_releasable
end