create_github_release 0.2.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.markdownlint.yml +1 -1
  3. data/CHANGELOG.md +21 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +254 -32
  6. data/create_github_release.gemspec +6 -2
  7. data/exe/create-github-release +12 -8
  8. data/lib/create_github_release/assertion_base.rb +25 -11
  9. data/lib/create_github_release/assertions/bundle_is_up_to_date.rb +4 -3
  10. data/lib/create_github_release/assertions/{docker_is_running.rb → gh_authenticated.rb} +9 -8
  11. data/lib/create_github_release/assertions/gh_command_exists.rb +3 -2
  12. data/lib/create_github_release/assertions/git_command_exists.rb +3 -2
  13. data/lib/create_github_release/assertions/in_git_repo.rb +3 -2
  14. data/lib/create_github_release/assertions/in_repo_root_directory.rb +3 -2
  15. data/lib/create_github_release/assertions/last_release_tag_exists.rb +47 -0
  16. data/lib/create_github_release/assertions/local_and_remote_on_same_commit.rb +4 -3
  17. data/lib/create_github_release/assertions/local_release_branch_does_not_exist.rb +6 -5
  18. data/lib/create_github_release/assertions/local_release_tag_does_not_exist.rb +3 -3
  19. data/lib/create_github_release/assertions/no_staged_changes.rb +3 -2
  20. data/lib/create_github_release/assertions/no_uncommitted_changes.rb +3 -2
  21. data/lib/create_github_release/assertions/on_default_branch.rb +5 -4
  22. data/lib/create_github_release/assertions/remote_release_branch_does_not_exist.rb +6 -5
  23. data/lib/create_github_release/assertions/remote_release_tag_does_not_exist.rb +6 -5
  24. data/lib/create_github_release/assertions.rb +2 -2
  25. data/lib/create_github_release/backtick_debug.rb +69 -0
  26. data/lib/create_github_release/change.rb +73 -0
  27. data/lib/create_github_release/changelog.rb +40 -68
  28. data/lib/create_github_release/command_line_options.rb +367 -0
  29. data/lib/create_github_release/command_line_parser.rb +113 -25
  30. data/lib/create_github_release/project.rb +868 -0
  31. data/lib/create_github_release/release_assertions.rb +3 -3
  32. data/lib/create_github_release/release_tasks.rb +1 -1
  33. data/lib/create_github_release/task_base.rb +25 -11
  34. data/lib/create_github_release/tasks/commit_release.rb +4 -3
  35. data/lib/create_github_release/tasks/create_github_release.rb +16 -35
  36. data/lib/create_github_release/tasks/create_release_branch.rb +6 -5
  37. data/lib/create_github_release/tasks/create_release_pull_request.rb +10 -42
  38. data/lib/create_github_release/tasks/create_release_tag.rb +6 -5
  39. data/lib/create_github_release/tasks/push_release.rb +5 -4
  40. data/lib/create_github_release/tasks/update_changelog.rb +16 -67
  41. data/lib/create_github_release/tasks/update_version.rb +33 -5
  42. data/lib/create_github_release/version.rb +1 -1
  43. data/lib/create_github_release.rb +4 -2
  44. metadata +27 -10
  45. data/.vscode/settings.json +0 -7
  46. data/lib/create_github_release/assertions/changelog_docker_container_exists.rb +0 -73
  47. data/lib/create_github_release/options.rb +0 -397
  48. data/lib/create_github_release/release.rb +0 -82
@@ -0,0 +1,868 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bump'
4
+ require 'uri'
5
+
6
+ module CreateGithubRelease
7
+ # rubocop:disable Metrics/ClassLength
8
+
9
+ # Captures the options for this script
10
+ #
11
+ # @api public
12
+ #
13
+ class Project
14
+ # Initialize a new Project
15
+ #
16
+ # Project attributes are set using one of these methods:
17
+ # 1. The attribute is explicitly set using an attribute writer
18
+ # 2. The attribute is set using the options object
19
+ # 3. The attribute is set using the default value or running some command
20
+ #
21
+ # Method 1 takes precedence, then method 2, then method 3.
22
+ #
23
+ # The recommended way to set the attributes is to use method 2 to override values
24
+ # and otherwise method 3 to use the default value. Method 1 is only recommended for testing
25
+ # or if there is no other way to accomplish what you need. Method 1 should ONLY be
26
+ # used in the block passed to the initializer.
27
+ #
28
+ # @example calling `.new` without a block
29
+ # options = CreateGithubRelease::CommandLineOptions.new { |o| o.release_type = 'minor' }
30
+ # project = CreateGithubRelease::Project.new(options)
31
+ # options.release_type = 'minor'
32
+ #
33
+ # @example calling `.new` with a block
34
+ # options = CreateGithubRelease::CommandLineOptions.new { |o| o.release_type = 'minor' }
35
+ # project = CreateGithubRelease::Project.new(options) do |p|
36
+ # p.release_type = 'major'
37
+ # end
38
+ # options.release_type = 'major'
39
+ #
40
+ # @param options [CreateGithubRelease::CommandLineOptions] the options to initialize the instance with
41
+ #
42
+ # @yield [self] an initialization block
43
+ # @yieldparam self [CreateGithubRelease::Project] the instance being initialized aka `self`
44
+ # @yieldreturn [void] the return value is ignored
45
+ #
46
+ def initialize(options)
47
+ @options = options
48
+ yield self if block_given?
49
+
50
+ setup_first_release if release_type == 'first'
51
+ end
52
+
53
+ # @!attribute options [r]
54
+ #
55
+ # The command line options used to initialize this project
56
+ #
57
+ # @example
58
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
59
+ # project = CreateGithubRelease::Project.new(options)
60
+ # project.options == options #=> true
61
+ #
62
+ # @return [CreateGithubRelease::CommandLineOptions]
63
+ #
64
+ attr_reader :options
65
+
66
+ attr_writer \
67
+ :default_branch, :next_release_tag, :next_release_date, :next_release_version,
68
+ :last_release_tag, :last_release_version, :release_branch, :release_log_url,
69
+ :release_type, :release_url, :remote, :remote_base_url, :remote_repository, :remote_url,
70
+ :changelog_path, :changes, :next_release_description, :last_release_changelog,
71
+ :next_release_changelog, :first_commit, :verbose, :quiet
72
+
73
+ # attr_writer :first_release
74
+
75
+ # @!attribute [rw] default_branch
76
+ #
77
+ # The default branch of the remote repository
78
+ #
79
+ # This is the default branch as reported by the `HEAD branch` returned by
80
+ # `git remote show #{remote}`.
81
+ #
82
+ # Uses the value of `remote` to determine the remote repository to query.
83
+ #
84
+ # @example By default, `default_branch` is based on `git remote show`
85
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
86
+ # project = CreateGithubRelease::Project.new(options)
87
+ # options.default_branch # => 'main'
88
+ #
89
+ # @example `default_branch` can be set explicitly
90
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
91
+ # project = CreateGithubRelease::Project.new(options)
92
+ # project.default_branch = 'master'
93
+ # project.default_branch #=> 'master'
94
+ #
95
+ # @return [String]
96
+ #
97
+ # @raise [RuntimeError] if the git command fails
98
+ #
99
+ # @api public
100
+ #
101
+ def default_branch
102
+ @default_branch ||= options.default_branch || begin
103
+ output = `git remote show '#{remote}'`
104
+ raise "Could not determine default branch for remote '#{remote}'" unless $CHILD_STATUS.success?
105
+
106
+ output.match(/HEAD branch: (.*?)$/)[1]
107
+ end
108
+ end
109
+
110
+ # @!attribute [rw] next_release_tag
111
+ #
112
+ # The tag to use for the next release
113
+ #
114
+ # Uses the value of `next_release_version` to determine the tag name.
115
+ #
116
+ # @example By default, `next_release_tag` is based on `next_release_version`
117
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
118
+ # project = CreateGithubRelease::Project.new(options)
119
+ # project.next_release_version = '1.0.0'
120
+ # project.next_relase_tag #=> 'v1.0.0'
121
+ #
122
+ # @example `next_tag` can be set explicitly
123
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
124
+ # project = CreateGithubRelease::Project.new(options)
125
+ # project.next_release_tag = 'v1.0.0'
126
+ # project.next_relase_tag #=> 'v1.0.0'
127
+ #
128
+ # @return [String]
129
+ #
130
+ # @api public
131
+ #
132
+ def next_release_tag
133
+ @next_release_tag ||= "v#{next_release_version}"
134
+ end
135
+
136
+ # @!attribute [rw] next_release_date
137
+ #
138
+ # The date next_release_tag was created
139
+ #
140
+ # If the next_release_tag does not exist, Date.today is returned.
141
+ #
142
+ # @example By default, `next_release_date` is based on `next_release_tag`
143
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
144
+ # project = CreateGithubRelease::Project.new(options)
145
+ # project.next_release_tag = 'v1.0.0'
146
+ # project.next_release_date #=> #<Date: 2023-02-01 ((2459189j,0s,0n),+0s,2299161j)>
147
+ #
148
+ # @example It can also be set explicitly
149
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
150
+ # project = CreateGithubRelease::Project.new(options)
151
+ # project.next_release_date = Date.new(2023, 2, 1)
152
+ # project.next_release_date #=> #<Date: 2023-02-01 ((2459189j,0s,0n),+0s,2299161j)>
153
+ #
154
+ # @return [Date]
155
+ #
156
+ # @raise [RuntimeError] if the git command fails
157
+ #
158
+ # @api public
159
+ #
160
+ def next_release_date
161
+ @next_release_date ||=
162
+ if tag_exist?(next_release_tag)
163
+ date = `git show --format=format:%aI --quiet "#{next_release_tag}"`
164
+ raise "Could not determine date for tag '#{next_release_tag}'" unless $CHILD_STATUS.success?
165
+
166
+ Date.parse(date.chomp)
167
+ else
168
+ Date.today
169
+ end
170
+ end
171
+
172
+ # `true` if the given tag exists in the local repository
173
+ #
174
+ # @example
175
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
176
+ # project = CreateGithubRelease::Project.new(options)
177
+ # project.tag_exist?('v1.0.0') #=> false
178
+ #
179
+ # @param tag [String] the tag to check
180
+ #
181
+ # @return [Boolean]
182
+ #
183
+ # @api public
184
+ #
185
+ def tag_exist?(tag)
186
+ tags = `git tag --list "#{tag}"`.chomp
187
+ raise 'Could not list tags' unless $CHILD_STATUS.success?
188
+
189
+ !tags.empty?
190
+ end
191
+
192
+ # @!attribute [rw] next_release_version
193
+ #
194
+ # The version of the next release
195
+ #
196
+ # @example By default, `next_release_version` is based on the value returned by `bump show-next <release_type>`
197
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
198
+ # project = CreateGithubRelease::Project.new(options)
199
+ # project.next_release_version #=> '1.0.0'
200
+ #
201
+ # @example It can also be set explicitly
202
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
203
+ # project = CreateGithubRelease::Project.new(options)
204
+ # project.next_release_version = '1.0.0
205
+ # project.next_release_version #=> '1.0.0'
206
+ #
207
+ # @return [String]
208
+ #
209
+ # @raise [RuntimeError] if the bump command fails
210
+ #
211
+ # @api public
212
+ #
213
+ def next_release_version
214
+ @next_release_version ||= options.next_release_version || bump_show_next_version
215
+ end
216
+
217
+ # @!attribute [rw] last_release_tag
218
+ #
219
+ # The tag to used for the last release
220
+ #
221
+ # Uses the value of `last_release_version` to determine the tag name.
222
+ #
223
+ # @example By default, `last_release_tag` is based on `last_release_version`
224
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
225
+ # project = CreateGithubRelease::Project.new(options)
226
+ # project.last_release_version = '0.0.1'
227
+ # project.last_relase_tag #=> 'v0.0.1'
228
+ #
229
+ # @example `last_release_tag` can be set explicitly
230
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
231
+ # project = CreateGithubRelease::Project.new(options)
232
+ # project.last_release_tag = 'v0.0.1'
233
+ # project.last_relase_tag #=> 'v0.0.1'
234
+ #
235
+ # @return [String]
236
+ #
237
+ # @api public
238
+ #
239
+ def last_release_tag
240
+ @last_release_tag ||= (first_release? ? '' : "v#{last_release_version}")
241
+ end
242
+
243
+ # @!attribute [rw] last_release_version
244
+ #
245
+ # The version of the last release
246
+ #
247
+ # @example By default, `last_release_version` is based on the value returned by `bump current`
248
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
249
+ # project = CreateGithubRelease::Project.new(options)
250
+ # project.last_release_version #=> '0.0.1'
251
+ #
252
+ # @example It can also be set explicitly
253
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
254
+ # project = CreateGithubRelease::Project.new(options)
255
+ # project.last_release_version = '0.0.1
256
+ # project.last_release_version #=> '0.0.1'
257
+ #
258
+ # @return [String]
259
+ #
260
+ # @raise [RuntimeError] if the bump command fails
261
+ #
262
+ # @api public
263
+ #
264
+ def last_release_version
265
+ @last_release_version ||= options.last_release_version || bump_current_version
266
+ end
267
+
268
+ # @!attribute [rw] release_branch
269
+ #
270
+ # The name of the release branch being created
271
+ #
272
+ # @example By default, `release_branch` is based on the value returned by `next_release_tag`
273
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
274
+ # project = CreateGithubRelease::Project.new(options)
275
+ # project.next_release_tag = 'v1.0.0'
276
+ # project.release_branch #=> 'release-v1.0.0'
277
+ #
278
+ # @example It can also be set explicitly
279
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
280
+ # project = CreateGithubRelease::Project.new(options)
281
+ # project.next_release_branch = 'release-v1.0.0'
282
+ # project.next_release_branch #=> 'release-v1.0.0'
283
+ #
284
+ # @return [String]
285
+ #
286
+ # @raise [RuntimeError] if the bump command fails
287
+ #
288
+ # @api public
289
+ #
290
+ def release_branch
291
+ @release_branch ||= options.release_branch || "release-#{next_release_tag}"
292
+ end
293
+
294
+ # @!attribute [rw] release_log_url
295
+ #
296
+ # The URL of the page containing a list of the changes in the release
297
+ #
298
+ # @example By default, `release_log_url` is based on `remote_url`, `last_release_tag`, and `next_release_tag`
299
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
300
+ # project = CreateGithubRelease::Project.new(options)
301
+ # project.remote_url = URI.parse('https://github.com/org/repo')
302
+ # project.last_release_tag = 'v0.0.1'
303
+ # project.next_release_tag = 'v1.0.0'
304
+ # project.release_log_url #=> #<URI::HTTPS https://github.com/org/repo/compare/v0.0.1..v1.0.0>
305
+ #
306
+ # @example It can also be set explicitly
307
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
308
+ # project = CreateGithubRelease::Project.new(options)
309
+ # project.release_log_url = URI.parse('https://github.com/org/repo/compare/v0.0.1..v1.0.0')
310
+ # project.release_log_url #=> #<URI::HTTPS https://github.com/org/repo/compare/v0.0.1..v1.0.0>
311
+ #
312
+ # @return [URI]
313
+ #
314
+ # @raise [RuntimeError] if the bump command fails
315
+ #
316
+ # @api public
317
+ #
318
+ def release_log_url
319
+ @release_log_url ||= begin
320
+ from = first_release? ? first_commit : last_release_tag
321
+ to = next_release_tag
322
+ URI.parse("#{remote_url}/compare/#{from}..#{to}")
323
+ end
324
+ end
325
+
326
+ # @!attribute [rw] release_type
327
+ #
328
+ # The type of the release being created (e.g. 'major', 'minor', 'patch')
329
+ #
330
+ # @note this must be one of the values accepted by the `bump` command
331
+ #
332
+ # @example By default, this value comes from the options object
333
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
334
+ # project = CreateGithubRelease::Project.new(options)
335
+ # project.release_type #=> 'major'
336
+ #
337
+ # @example It can also be set explicitly
338
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
339
+ # project = CreateGithubRelease::Project.new(options)
340
+ # project.release_type = 'patch'
341
+ # project.release_type #=> 'patch'
342
+ #
343
+ # @return [String]
344
+ #
345
+ # @raise [ArgumentError] if a release type was not provided
346
+ #
347
+ # @api public
348
+ #
349
+ def release_type
350
+ @release_type ||= options.release_type || raise(ArgumentError, 'release_type is required')
351
+ end
352
+
353
+ # @!attribute [rw] release_url
354
+ #
355
+ # The URL of the page containing a list of the changes in the release
356
+ #
357
+ # @example By default, `release_url` is based on `remote_url` and `next_release_tag`
358
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
359
+ # project = CreateGithubRelease::Project.new(options)
360
+ # project.remote_url = URI.parse('https://github.com/org/repo')
361
+ # project.next_release_tag = 'v1.0.0'
362
+ # project.release_url #=> #<URI::HTTPS https://github.com/org/repo/releases/tag/v1.0.0>
363
+ #
364
+ # @example It can also be set explicitly
365
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
366
+ # project = CreateGithubRelease::Project.new(options)
367
+ # project.release_url = URI.parse('https://github.com/org/repo/releases/tag/v1.0.0')
368
+ # project.release_url #=> #<URI::HTTPS https://github.com/org/repo/releases/tag/v1.0.0>
369
+ #
370
+ # @return [URI]
371
+ #
372
+ # @api public
373
+ #
374
+ def release_url
375
+ @release_url ||= URI.parse("#{remote_url}/releases/tag/#{next_release_tag}")
376
+ end
377
+
378
+ # @!attribute [rw] remote
379
+ #
380
+ # The git remote used to determine the repository url
381
+ #
382
+ # @example By default, 'origin' is used
383
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
384
+ # project = CreateGithubRelease::Project.new(options)
385
+ # project.remote #=> 'origin'
386
+ #
387
+ # @example It can also be set in the options
388
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major', remote: 'upstream')
389
+ # project = CreateGithubRelease::Project.new(options)
390
+ # project.remote #=> 'upstream'
391
+ #
392
+ # @example It can also be set explicitly
393
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
394
+ # project = CreateGithubRelease::Project.new(options)
395
+ # project.remote = 'upstream'
396
+ # project.remote #=> 'upstream'
397
+ #
398
+ # @return [String]
399
+ #
400
+ # @api public
401
+ #
402
+ def remote
403
+ @remote ||= options.remote || 'origin'
404
+ end
405
+
406
+ # @!attribute [rw] remote_base_url
407
+ #
408
+ # The base part of the remote url (e.g. 'https://github.com/')
409
+ #
410
+ # @example By default, this value is based on `remote_url`
411
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
412
+ # project = CreateGithubRelease::Project.new(options)
413
+ # project.remote_url = URI.parse('https://github.com/org/repo')
414
+ # project.remote #=> #<URI::HTTPS https://github.com/>
415
+ #
416
+ # @example It can also be set explicitly
417
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
418
+ # project = CreateGithubRelease::Project.new(options)
419
+ # project.remote_base_url = URI.parse('https://github.com/')
420
+ # project.remote_base_url #=> #<URI::HTTPS https://github.com/>
421
+ #
422
+ # @return [URI]
423
+ #
424
+ # @api public
425
+ #
426
+ def remote_base_url
427
+ @remote_base_url ||= URI.parse(remote_url.to_s[0..-remote_url.path.length])
428
+ end
429
+
430
+ # @!attribute [rw] remote_repository
431
+ #
432
+ # The git remote owner and repository name (e.g. 'org/repo')
433
+ #
434
+ # @example By default, this value is based on `remote_url`
435
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
436
+ # project = CreateGithubRelease::Project.new(options)
437
+ # project.remote_url = URI.parse('htps://github.com/org/repo')
438
+ # project.remote_repository #=> 'org/repo'
439
+ #
440
+ # @example It can also be set explicitly
441
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
442
+ # project = CreateGithubRelease::Project.new(options)
443
+ # project.remote_repository = 'org/repo'
444
+ #
445
+ # @return [String]
446
+ #
447
+ # @api public
448
+ #
449
+ def remote_repository
450
+ @remote_repository ||= remote_url.path.sub(%r{^/}, '').sub(/\.git$/, '')
451
+ end
452
+
453
+ # @!attribute [rw] remote_url
454
+ #
455
+ # The URL of the git remote repository (e.g. 'https://github.com/org/repo')
456
+ #
457
+ # @example By default, this value is based on `remote` and the `git remote get-url` command
458
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
459
+ # project = CreateGithubRelease::Project.new(options)
460
+ # project.remote #=> #<URI::HTTPS https://github.com/org/repo>
461
+ #
462
+ # @example It can also be set explicitly
463
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
464
+ # project = CreateGithubRelease::Project.new(options)
465
+ # project.remote_url = URI.parse('https://github.com/org/repo')
466
+ # project.remote_url #=> #<URI::HTTPS https://github.com/org/repo>
467
+ #
468
+ # @return [URI]
469
+ #
470
+ # @api public
471
+ #
472
+ def remote_url
473
+ @remote_url ||= begin
474
+ remote_url_string = `git remote get-url '#{remote}'`
475
+ raise "Could not determine remote url for remote '#{remote}'" unless $CHILD_STATUS.success?
476
+
477
+ remote_url_string = remote_url_string.chomp
478
+ remote_url_string = remote_url_string[0..-5] if remote_url_string.end_with?('.git')
479
+ URI.parse(remote_url_string)
480
+ end
481
+ end
482
+
483
+ # @!attribute [rw] changelog_path
484
+ #
485
+ # The path relative to the project root where the changelog is located
486
+ #
487
+ # @example By default, this value is 'CHANGELOG.md'
488
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
489
+ # project = CreateGithubRelease::Project.new(options)
490
+ # project.changelog_path #=> 'CHANGELOG.md'
491
+ #
492
+ # @example It can also be set in the options
493
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major', changelog_path: 'docs/CHANGES.txt')
494
+ # project = CreateGithubRelease::Project.new(options)
495
+ # project.remote_repository = 'docs/CHANGES.txt'
496
+ #
497
+ # @example It can also be set explicitly
498
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
499
+ # project = CreateGithubRelease::Project.new(options)
500
+ # project.changelog_path = 'docs/CHANGES.txt'
501
+ # project.remote_repository = 'docs/CHANGES.txt'
502
+ #
503
+ # @return [String]
504
+ #
505
+ # @api public
506
+ #
507
+ def changelog_path
508
+ @changelog_path ||= options.changelog_path || 'CHANGELOG.md'
509
+ end
510
+
511
+ # @!attribute [rw] changes
512
+ #
513
+ # An array containing the changes since the last_release_tag
514
+ #
515
+ # Calls `git log HEAD <next_release_tag>` to list the changes.
516
+ #
517
+ # @example By default, uses `git log`
518
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
519
+ # project = CreateGithubRelease::Project.new(options)
520
+ # pp project.changes
521
+ # [
522
+ # #<CreateGithubRelease::Change:0x00000001084b92f0 @sha="24bdd02", @subject="Foo feature">,
523
+ # #<CreateGithubRelease::Change:0x00000001084b93e0 @sha="d75e1e9", @subject="Bar feature">
524
+ # ]
525
+ #
526
+ # @example It can also be set explicitly
527
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
528
+ # project = CreateGithubRelease::Project.new(options)
529
+ # project.changes = 'All the changes'
530
+ # project.changes #=> 'All the changes'
531
+ #
532
+ # @return [Array<CreateGithubRelease>]
533
+ #
534
+ # @api public
535
+ #
536
+ def changes
537
+ @changes ||= begin
538
+ tip = "'HEAD'"
539
+ base = first_release? ? '' : "'^#{last_release_tag}' "
540
+ command = "git log #{tip} #{base}--oneline --format='format:%h\t%s'"
541
+ git_log = `#{command}`
542
+ raise "Could not determine changes since #{last_release_tag}" unless $CHILD_STATUS.success?
543
+
544
+ git_log.split("\n").map { |l| ::CreateGithubRelease::Change.new(*l.split("\t")) }
545
+ end
546
+ end
547
+
548
+ # @!attribute [rw] next_release_description
549
+ #
550
+ # The formatted release description
551
+ #
552
+ # @example
553
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
554
+ # project = CreateGithubRelease::Project.new(options) do |p|
555
+ # p.remote_url = URI.parse('https://github.com/username/repo')
556
+ # p.last_release_tag = 'v0.1.0'
557
+ # p.next_release_tag = 'v1.0.0'
558
+ # p.next_release_date = Date.new(2022, 11, 7)
559
+ # p.changes = [
560
+ # CreateGithubRelease::Change.new('e718690', 'Release v1.0.0 (#3)'),
561
+ # CreateGithubRelease::Change.new('ab598f3', 'Fix Rubocop offenses (#2)')
562
+ # ]
563
+ # end
564
+ # puts project.next_release_description
565
+ # ## v1.0.0 (2022-11-07)
566
+ #
567
+ # [Full Changelog](https://github.com/username/repo/compare/v0.1.0...v1.0.0
568
+ #
569
+ # * e718690 Release v1.0.0 (#3)
570
+ # * ab598f3 Fix Rubocop offenses (#2)
571
+ #
572
+ # @return [String]
573
+ #
574
+ # @api public
575
+ #
576
+ def next_release_description
577
+ @next_release_description ||= begin
578
+ header = first_release? ? 'Changes:' : "Changes since #{last_release_tag}:"
579
+ <<~DESCRIPTION
580
+ ## #{next_release_tag} (#{next_release_date.strftime('%Y-%m-%d')})
581
+
582
+ [Full Changelog](#{release_log_url})
583
+
584
+ #{header}
585
+
586
+ #{list_of_changes}
587
+ DESCRIPTION
588
+ end
589
+ end
590
+
591
+ # @!attribute [rw] last_release_changelog
592
+ #
593
+ # The existing changelog (of the last release) as a string
594
+ #
595
+ # @example
596
+ # changelog_path = 'TEST_CHANGELOG.md'
597
+ # File.write(changelog_path, <<~CHANGELOG)
598
+ # # Project Changelog
599
+ #
600
+ # ## v0.1.0 (2021-11-07)
601
+ #
602
+ # * e718690 Release v0.1.0 (#3)
603
+ # CHANGELOG
604
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
605
+ # project = CreateGithubRelease::Project.new(options) do |p|
606
+ # p.changelog_path = changelog_path
607
+ # end
608
+ # puts project.last_release_changelog
609
+ # # Project Changelog
610
+ #
611
+ # ## v0.1.0 (2021-11-07)
612
+ #
613
+ # * e718690 Release v0.1.0 (#3)
614
+ #
615
+ # @return [String]
616
+ #
617
+ # @api public
618
+ #
619
+ def last_release_changelog
620
+ @last_release_changelog ||= begin
621
+ File.read(changelog_path)
622
+ rescue Errno::ENOENT
623
+ ''
624
+ rescue StandardError
625
+ raise 'Could not read the changelog file'
626
+ end
627
+ end
628
+
629
+ # @!attribute [rw] next_release_changelog
630
+ #
631
+ # The changelog of the next release as a string
632
+ #
633
+ # This is the result of inserting next_release_description into
634
+ # last_release_changelog.
635
+ #
636
+ # @example
637
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
638
+ # project = CreateGithubRelease::Project.new(options) do |p|
639
+ # p.last_release_changelog = <<~CHANGELOG
640
+ # # Project Changelog
641
+ #
642
+ # ## v0.1.0 (2021-11-07)
643
+ #
644
+ # * e718690 Release v0.1.0 (#3)
645
+ # CHANGELOG
646
+ # p.next_release_description = <<~next_release_description
647
+ # ## v1.0.0 (2022-11-07)
648
+ #
649
+ # [Full Changelog](http://github.com/org/repo/compare/v0.1.0...v1.0.0)
650
+ #
651
+ # * e718690 Release v1.0.0 (#3)
652
+ # * ab598f3 Add the FizzBuzz Feature (#2)
653
+ # next_release_description
654
+ # end
655
+ # puts project.next_release_changelog
656
+ #
657
+ # @return [String]
658
+ #
659
+ # @api public
660
+ #
661
+ def next_release_changelog
662
+ @next_release_changelog ||=
663
+ CreateGithubRelease::Changelog.new(last_release_changelog, next_release_description).to_s
664
+ end
665
+
666
+ # Show the project details as a string
667
+ #
668
+ # @example
669
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
670
+ # project = CreateGithubRelease::Project.new(options)
671
+ # puts projects.to_s
672
+ # default_branch: main
673
+ # next_release_tag: v1.0.0
674
+ # next_release_date: 2023-02-01
675
+ # next_release_version: 1.0.0
676
+ # last_release_tag: v0.1.0
677
+ # last_release_version: 0.1.0
678
+ # release_branch: release-v1.0.0
679
+ # release_log_url: https://github.com/org/repo/compare/v0.1.0..v1.0.0
680
+ # release_type: major
681
+ # release_url: https://github.com/org/repo/releases/tag/v1.0.0
682
+ # remote: origin
683
+ # remote_base_url: https://github.com/
684
+ # remote_repository: org/repo
685
+ # remote_url: https://github.com/org/repo
686
+ # changelog_path: CHANGELOG.md
687
+ # verbose?: false
688
+ # quiet?: false
689
+ #
690
+ # @return [String]
691
+ #
692
+ # @api public
693
+ #
694
+ def to_s
695
+ <<~OUTPUT
696
+ first_release: #{first_release}
697
+ default_branch: #{default_branch}
698
+ next_release_tag: #{next_release_tag}
699
+ next_release_date: #{next_release_date}
700
+ next_release_version: #{next_release_version}
701
+ last_release_tag: #{last_release_tag}
702
+ last_release_version: #{last_release_version}
703
+ release_branch: #{release_branch}
704
+ release_log_url: #{release_log_url}
705
+ release_type: #{release_type}
706
+ release_url: #{release_url}
707
+ remote: #{remote}
708
+ remote_base_url: #{remote_base_url}
709
+ remote_repository: #{remote_repository}
710
+ remote_url: #{remote_url}
711
+ verbose?: #{verbose?}
712
+ quiet?: #{quiet?}
713
+ OUTPUT
714
+ end
715
+
716
+ # @!attribute [rw] verbose
717
+ #
718
+ # If `true` enables verbose output
719
+ #
720
+ # @example By default, this value is based on the `verbose` option
721
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major', verbose: true)
722
+ # project = CreateGithubRelease::Project.new(options)
723
+ # project.verbose? #=> true
724
+ #
725
+ # @example It can also be set explicitly
726
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
727
+ # project = CreateGithubRelease::Project.new(options)
728
+ # project.verbose = true
729
+ # project.verbose? #=> true
730
+ #
731
+ # @return [Boolean]
732
+ #
733
+ # @api public
734
+ #
735
+ def verbose
736
+ @verbose ||= options.verbose || false
737
+ end
738
+
739
+ alias verbose? verbose
740
+
741
+ # @!attribute [rw] quiet
742
+ #
743
+ # If `true` supresses all output
744
+ #
745
+ # @example By default, this value is based on the `quiet` option
746
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major', quiet: true)
747
+ # project = CreateGithubRelease::Project.new(options)
748
+ # project.quiet? #=> true
749
+ #
750
+ # @example It can also be set explicitly
751
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
752
+ # project = CreateGithubRelease::Project.new(options)
753
+ # project.quiet = true
754
+ # project.quiet? #=> true
755
+ #
756
+ # @return [Boolean]
757
+ #
758
+ # @api public
759
+ #
760
+ def quiet
761
+ @quiet ||= options.quiet || false
762
+ end
763
+
764
+ alias quiet? quiet
765
+
766
+ # @!attribute [rw] last_release_changelog
767
+ #
768
+ # true if release_type is 'first' otherwise false
769
+ #
770
+ # @example Returns true if release_type is 'first'
771
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'first')
772
+ # project = CreateGithubRelease::Project.new(options)
773
+ # project.first_release? #=> true
774
+ #
775
+ # @example Returnss false if release_type is not 'first'
776
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
777
+ # project = CreateGithubRelease::Project.new(options)
778
+ # project.first_release? #=> false
779
+ #
780
+ # @return [Boolean]
781
+ #
782
+ # @api public
783
+ #
784
+ def first_release
785
+ @first_release ||= release_type == 'first'
786
+ end
787
+
788
+ alias first_release? first_release
789
+
790
+ # @!attribute [rw] first_commit
791
+ #
792
+ # The SHA of the oldest commit that is an ancestor of HEAD
793
+ #
794
+ # @example
795
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
796
+ # project = CreateGithubRelease::Project.new(options)
797
+ # project.first_commit? #=> '1234567'
798
+ #
799
+ # @return [String]
800
+ #
801
+ # @api public
802
+ #
803
+ def first_commit
804
+ @first_commit ||= begin
805
+ command = "git log 'HEAD' --oneline --format='format:%h'"
806
+ git_log = `#{command}`
807
+ raise "Could not list changes from first commit up to #{last_release_tag}" unless $CHILD_STATUS.success?
808
+
809
+ git_log.split("\n").last.chomp
810
+ end
811
+ end
812
+
813
+ private
814
+
815
+ # The current version of the project as determined by bump
816
+ # @return [String] The current version of the project
817
+ # @api private
818
+ def bump_current_version
819
+ output = `bump current`
820
+ raise 'Could not determine current version using bump' unless $CHILD_STATUS.success?
821
+
822
+ output.lines.last.chomp
823
+ end
824
+
825
+ # The next version of the project as determined by bump and release_type
826
+ # @return [String] The next version of the project
827
+ # @api private
828
+ def bump_show_next_version
829
+ output = `bump show-next #{release_type}`
830
+ raise 'Could not determine next version using bump' unless $CHILD_STATUS.success?
831
+
832
+ output.lines.last.chomp
833
+ end
834
+
835
+ # Setup versions and tags for a first release
836
+ # @return [Void]
837
+ # @api private
838
+ def setup_first_release
839
+ self.next_release_version = @next_release_version || bump_current_version
840
+ self.last_release_version = ''
841
+ self.last_release_tag = ''
842
+ end
843
+
844
+ # The list of changes in the release as a string
845
+ # @return [String] The list of changes in the release as a string
846
+ # @api private
847
+ def list_of_changes
848
+ return '* No changes' if changes.empty?
849
+
850
+ changes.map do |change|
851
+ "* #{change.sha} #{change.subject}"
852
+ end.join("\n")
853
+ end
854
+
855
+ # `true` if the `#verbose?` flag is `true`
856
+ # @return [Boolean]
857
+ # @api private
858
+ def backtick_debug?
859
+ verbose?
860
+ end
861
+
862
+ # Override the backtick operator for this class to call super and output
863
+ # debug information if `verbose?` is true
864
+ include CreateGithubRelease::BacktickDebug
865
+ end
866
+
867
+ # rubocop:enable Metrics/ClassLength
868
+ end