create_github_release 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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