Class: GitHubChangelogGenerator::ChangelogGenerator

Inherits:
Object
  • Object
show all
Defined in:
lib/github_changelog_generator.rb

Overview

Main class and entry point for this script.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeObject

Class, responsible for whole change log generation cycle



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/github_changelog_generator.rb', line 25

def initialize
  @options = Parser.parse_options

  @fetcher = GitHubChangelogGenerator::Fetcher.new @options

  @generator = Generator.new @options

  # @all_tags = get_filtered_tags
  @all_tags = @fetcher.get_all_tags

  @issues, @pull_requests = @fetcher.fetch_issues_and_pull_requests

  @pull_requests = @options[:pulls] ? get_filtered_pull_requests : []

  @issues = @options[:issues] ? get_filtered_issues : []

  fetch_event_for_issues_and_pr
  detect_actual_closed_dates
end

Instance Attribute Details

#all_tagsObject

Returns the value of attribute all_tags.



21
22
23
# File 'lib/github_changelog_generator.rb', line 21

def all_tags
  @all_tags
end

#githubObject

Returns the value of attribute github.



21
22
23
# File 'lib/github_changelog_generator.rb', line 21

def github
  @github
end

#optionsObject

Returns the value of attribute options.



21
22
23
# File 'lib/github_changelog_generator.rb', line 21

def options
  @options
end

Instance Method Details

#compound_changelogObject

The entry point of this script to generate change log

Raises:



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
216
217
218
219
220
221
# File 'lib/github_changelog_generator.rb', line 187

def compound_changelog
  log = "# Change Log\n\n"

  if @options[:unreleased_only]
    log += generate_log_between_tags(all_tags[0], nil)
  elsif @options[:tag1] and @options[:tag2]
    tag1 = @options[:tag1]
    tag2 = @options[:tag2]
    tags_strings = []
    all_tags.each { |x| tags_strings.push(x["name"]) }

    if tags_strings.include?(tag1)
      if tags_strings.include?(tag2)
        to_a = tags_strings.map.with_index.to_a
        hash = Hash[to_a]
        index1 = hash[tag1]
        index2 = hash[tag2]
        log += generate_log_between_tags(all_tags[index1], all_tags[index2])
      else
        fail ChangelogGeneratorError, "Can't find tag #{tag2} -> exit".red
      end
    else
      fail ChangelogGeneratorError, "Can't find tag #{tag1} -> exit".red
    end
  else
    log += generate_log_for_all_tags
  end

  log += "\n\n\\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*"

  output_filename = "#{@options[:output]}"
  File.open(output_filename, "w") { |file| file.write(log) }
  puts "Done!"
  puts "Generated log placed in #{`pwd`.strip!}/#{output_filename}"
end

#create_log(pull_requests, issues, newer_tag, older_tag_name = nil) ⇒ String

Generates log for section with header and body

Parameters:

  • pull_requests (Array)

    List or PR’s in new section

  • issues (Array)

    List of issues in new section

  • newer_tag (String)

    Name of the newer tag. Could be nil for ‘Unreleased` section

  • older_tag_name (String) (defaults to: nil)

    Older tag, used for the links. Could be nil for last tag.

Returns:

  • (String)

    Ready and parsed section



389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/github_changelog_generator.rb', line 389

def create_log(pull_requests, issues, newer_tag, older_tag_name = nil)
  newer_tag_time = newer_tag.nil? ? Time.new : @fetcher.get_time_of_tag(newer_tag)
  newer_tag_name = newer_tag.nil? ? @options[:unreleased_label] : newer_tag["name"]
  newer_tag_link = newer_tag.nil? ? "HEAD" : newer_tag_name

  github_site = options[:github_site] || "https://github.com"
  project_url = "#{github_site}/#{@options[:user]}/#{@options[:project]}"

  log = generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name, project_url)

  if @options[:issues]
    # Generate issues:
    issues_a = []
    enhancement_a = []
    bugs_a = []

    issues.each { |dict|
      added = false
      dict.labels.each { |label|
        if label.name == "bug"
          bugs_a.push dict
          added = true
          next
        end
        if label.name == "enhancement"
          enhancement_a.push dict
          added = true
          next
        end
      }
      unless added
        issues_a.push dict
      end
    }

    log += generate_sub_section(enhancement_a, @options[:enhancement_prefix])
    log += generate_sub_section(bugs_a, @options[:bug_prefix])
    log += generate_sub_section(issues_a, @options[:issue_prefix])
  end

  if @options[:pulls]
    # Generate pull requests:
    log += generate_sub_section(pull_requests, @options[:merge_prefix])
  end

  log
end

#delete_by_time(array, hash_key = :actual_date, older_tag = nil, newer_tag = nil) ⇒ Array

Method filter issues, that belong only specified tag range

Parameters:

  • array (Array)

    of issues to filter

  • hash_key (Symbol) (defaults to: :actual_date)

    key of date value default is :actual_date

  • older_tag (String) (defaults to: nil)

    all issues before this tag date will be excluded. May be nil, if it’s first tag

  • newer_tag (String) (defaults to: nil)

    all issue after this tag will be excluded. May be nil for unreleased section

Returns:

  • (Array)

    filtered issues



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/github_changelog_generator.rb', line 351

def delete_by_time(array, hash_key = :actual_date, older_tag = nil, newer_tag = nil)
  fail ChangelogGeneratorError, "At least one of the tags should be not nil!".red if older_tag.nil? && newer_tag.nil?

  newer_tag_time = newer_tag && @fetcher.get_time_of_tag(newer_tag)
  older_tag_time = older_tag && @fetcher.get_time_of_tag(older_tag)

  array.select { |req|
    if req[hash_key]
      t = Time.parse(req[hash_key]).utc

      if older_tag_time.nil?
        tag_in_range_old = true
      else
        tag_in_range_old = t > older_tag_time
      end

      if newer_tag_time.nil?
        tag_in_range_new = true
      else
        tag_in_range_new = t <= newer_tag_time
      end

      tag_in_range = (tag_in_range_old) && (tag_in_range_new)

      tag_in_range
    else
      false
    end
  }
end

#detect_actual_closed_datesObject



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/github_changelog_generator.rb', line 62

def detect_actual_closed_dates
  if @options[:verbose]
    print "Fetching closed dates for issues...\r"
  end

  threads = []

  @issues.each { |issue|
    threads << Thread.new {
      find_closed_date_by_commit(issue)
    }
  }

  @pull_requests.each { |pull_request|
    threads << Thread.new {
      find_closed_date_by_commit(pull_request)
    }
  }
  threads.each(&:join)

  if @options[:verbose]
    puts "Fetching closed dates for issues: Done!"
  end
end

#exclude_issues_by_labels(issues) ⇒ Array

delete all labels with labels from @options array

Parameters:

  • issues (Array)

Returns:

  • (Array)

    filtered array



176
177
178
179
180
181
182
183
# File 'lib/github_changelog_generator.rb', line 176

def exclude_issues_by_labels(issues)
  unless @options[:exclude_labels].nil?
    issues = issues.select { |issue|
      !(issue.labels.map(&:name) & @options[:exclude_labels]).any?
    }
  end
  issues
end

#fetch_event_for_issues_and_prArray

Fetch event for issues and pull requests

Returns:

  • (Array)

    array of fetched issues



501
502
503
504
505
506
507
508
509
# File 'lib/github_changelog_generator.rb', line 501

def fetch_event_for_issues_and_pr
  if @options[:verbose]
    print "Fetching events for issues and PR: 0/#{@issues.count + @pull_requests.count}\r"
  end

  # Async fetching events:

  @fetcher.fetch_events_async(@issues + @pull_requests)
end

#fetch_merged_at_pull_requestsObject

This method fetch missing required attributes for pull requests :merged_at - is a date, when issue PR was merged. More correct to use this date, not closed date.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/github_changelog_generator.rb', line 139

def fetch_merged_at_pull_requests
  if @options[:verbose]
    print "Fetching merged dates...\r"
  end
  pull_requests = @fetcher.fetch_pull_requests

  @pull_requests.each { |pr|
    fetched_pr = pull_requests.find { |fpr|
      fpr.number == pr.number
    }
    pr[:merged_at] = fetched_pr[:merged_at]
    pull_requests.delete(fetched_pr)
  }

  if @options[:verbose]
    puts "Fetching merged dates: Done!"
  end
end

#fetch_tags_datesObject

Async fetching of all tags dates



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
# File 'lib/github_changelog_generator.rb', line 258

def fetch_tags_dates
  if @options[:verbose]
    print "Fetching tag dates...\r"
  end

  # Async fetching tags:
  threads = []
  i = 0
  all = @all_tags.count
  @all_tags.each { |tag|
    threads << Thread.new {
      @fetcher.get_time_of_tag(tag)
      if @options[:verbose]
        print "Fetching tags dates: #{i + 1}/#{all}\r"
        i += 1
      end
    }
  }

  print "                                 \r"

  threads.each(&:join)

  if @options[:verbose]
    puts "Fetching tags dates: #{i} Done!"
  end
end

#filter_by_milestone(filtered_issues, newer_tag_name, src_array) ⇒ Object



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
# File 'lib/github_changelog_generator.rb', line 310

def filter_by_milestone(filtered_issues, newer_tag_name, src_array)
  filtered_issues.select! { |issue|
    # leave issues without milestones
    if issue.milestone.nil?
      true
    else
      # check, that this milestone in tag list:
      @all_tags.find { |tag| tag.name == issue.milestone.title }.nil?
    end
  }
  unless newer_tag_name.nil?

    # add missed issues (according milestones)
    issues_to_add = src_array.select { |issue|
      if issue.milestone.nil?
        false
      else
        # check, that this milestone in tag list:
        milestone_is_tag = @all_tags.find { |tag|
          tag.name == issue.milestone.title
        }

        if milestone_is_tag.nil?
          false
        else
          issue.milestone.title == newer_tag_name
        end
      end
    }

    filtered_issues |= issues_to_add
  end
  filtered_issues
end

#find_closed_date_by_commit(issue) ⇒ Object

Fill :actual_date parameter of specified issue by closed date of the commit, it it was closed by commit.

Parameters:

  • issue (Hash)


89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/github_changelog_generator.rb', line 89

def find_closed_date_by_commit(issue)
  unless issue["events"].nil?
    # if it's PR -> then find "merged event", in case of usual issue -> fond closed date
    compare_string = issue[:merged_at].nil? ? "closed" : "merged"
    # reverse! - to find latest closed event. (event goes in date order)
    issue["events"].reverse!.each { |event|
      if event[:event].eql? compare_string
        if event[:commit_id].nil?
          issue[:actual_date] = issue[:closed_at]
        else
          begin
            commit = @fetcher.fetch_commit(event)
            issue[:actual_date] = commit[:author][:date]
          rescue
            puts "Warning: Can't fetch commit #{event[:commit_id]}. It is probably referenced from another repo.".yellow
            issue[:actual_date] = issue[:closed_at]
          end
        end
        break
      end
    }
  end
  # TODO: assert issues, that remain without 'actual_date' hash for some reason.
end

#generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_link, project_url) ⇒ String

It generate one header for section with specific parameters.

Parameters:

  • newer_tag_name (String)
    • name of newer tag

  • newer_tag_link (String)
    • used for links. Could be same as #newer_tag_name or some specific value, like HEAD

  • newer_tag_time (Time)
    • time, when newer tag created

  • older_tag_link (String)
    • tag name, used for links.

  • project_url (String)
    • url for current project.

Returns:

  • (String)
    • Generate one ready-to-add section.



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/github_changelog_generator.rb', line 464

def generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_link, project_url)
  log = ""

  # Generate date string:
  time_string = newer_tag_time.strftime @options[:dateformat]

  # Generate tag name and link
  if newer_tag_name.equal? @options[:unreleased_label]
    log += "## [#{newer_tag_name}](#{project_url}/tree/#{newer_tag_link})\n\n"
  else
    log += "## [#{newer_tag_name}](#{project_url}/tree/#{newer_tag_link}) (#{time_string})\n\n"
  end

  if @options[:compare_link] && older_tag_link
    # Generate compare link
    log += "[Full Changelog](#{project_url}/compare/#{older_tag_link}...#{newer_tag_link})\n\n"
  end

  log
end

#generate_log_between_tags(older_tag, newer_tag) ⇒ Object

Generate log only between 2 specified tags

Parameters:

  • older_tag (String)

    all issues before this tag date will be excluded. May be nil, if it’s first tag

  • newer_tag (String)

    all issue after this tag will be excluded. May be nil for unreleased section



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/github_changelog_generator.rb', line 289

def generate_log_between_tags(older_tag, newer_tag)
  filtered_pull_requests = delete_by_time(@pull_requests, :actual_date, older_tag, newer_tag)
  filtered_issues = delete_by_time(@issues, :actual_date, older_tag, newer_tag)

  newer_tag_name = newer_tag.nil? ? nil : newer_tag["name"]
  older_tag_name = older_tag.nil? ? nil : older_tag["name"]

  if @options[:filter_issues_by_milestone]
    # delete excess irrelevant issues (according milestones)
    filtered_issues = filter_by_milestone(filtered_issues, newer_tag_name, @issues)
    filtered_pull_requests = filter_by_milestone(filtered_pull_requests, newer_tag_name, @pull_requests)
  end

  if filtered_issues.empty? && filtered_pull_requests.empty? && newer_tag.nil?
    # do not generate empty unreleased section
    return ""
  end

  create_log(filtered_pull_requests, filtered_issues, newer_tag, older_tag_name)
end

#generate_log_for_all_tagsString

The full cycle of generation for whole project

Returns:

  • (String)

    The complete change log



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
251
252
253
254
255
# File 'lib/github_changelog_generator.rb', line 225

def generate_log_for_all_tags
  fetch_tags_dates

  if @options[:verbose]
    puts "Sorting tags..."
  end

  @all_tags.sort_by! { |x| @fetcher.get_time_of_tag(x) }.reverse!

  if @options[:verbose]
    puts "Generating log..."
  end

  log = ""

  if @options[:unreleased] && @all_tags.count != 0
    unreleased_log = generate_log_between_tags(all_tags[0], nil)
    if unreleased_log
      log += unreleased_log
    end
  end

  (1...all_tags.size).each { |index|
    log += generate_log_between_tags(all_tags[index], all_tags[index - 1])
  }
  if @all_tags.count != 0
    log += generate_log_between_tags(nil, all_tags.last)
  end

  log
end

#generate_sub_section(issues, prefix) ⇒ String

Returns Generate ready-to-go sub-section.

Parameters:

  • issues (Array)

    List of issues on sub-section

  • prefix (String)

    Nae of sub-section

Returns:

  • (String)

    Generate ready-to-go sub-section



440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/github_changelog_generator.rb', line 440

def generate_sub_section(issues, prefix)
  log = ""

  if options[:simple_list] != true && issues.any?
    log += "#{prefix}\n\n"
  end

  if issues.any?
    issues.each { |issue|
      merge_string = @generator.get_string_for_issue(issue)
      log += "- #{merge_string}\n\n"
    }
  end
  log
end

#get_filtered_issuesArray

Filter issues according labels

Returns:

  • (Array)

    Filtered issues



487
488
489
490
491
492
493
494
495
496
497
# File 'lib/github_changelog_generator.rb', line 487

def get_filtered_issues
  filtered_issues = include_issues_by_labels(@issues)

  filtered_issues = exclude_issues_by_labels(filtered_issues)

  if @options[:verbose]
    puts "Filtered issues: #{filtered_issues.count}"
  end

  filtered_issues
end

#get_filtered_pull_requestsArray

This method fetches missing params for PR and filter them by specified options It include add all PR’s with labels from @options array And exclude all from :exclude_labels array.

Returns:

  • (Array)

    filtered PR’s



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/github_changelog_generator.rb', line 122

def get_filtered_pull_requests
  fetch_merged_at_pull_requests

  filtered_pull_requests = include_issues_by_labels(@pull_requests)

  filtered_pull_requests = exclude_issues_by_labels(filtered_pull_requests)

  if @options[:verbose]
    puts "Filtered pull requests: #{filtered_pull_requests.count}"
  end

  filtered_pull_requests
end

#get_filtered_tagsArray

Return tags after filtering tags in lists provided by option: –between-tags & –exclude-tags

Returns:

  • (Array)


48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/github_changelog_generator.rb', line 48

def get_filtered_tags
  all_tags = @fetcher.get_all_tags
  filtered_tags = []
  if @options[:between_tags]
    @options[:between_tags].each do |tag|
      unless all_tags.include? tag
        puts "Warning: can't find tag #{tag}, specified with --between-tags option.".yellow
      end
    end
    filtered_tags = all_tags.select { |tag| @options[:between_tags].include? tag }
  end
  filtered_tags
end

#include_issues_by_labels(issues) ⇒ Array

Include issues with labels, specified in :include_labels

Parameters:

  • issues (Array)

    to filter

Returns:

  • (Array)

    filtered array of issues



161
162
163
164
165
166
167
168
169
170
171
# File 'lib/github_changelog_generator.rb', line 161

def include_issues_by_labels(issues)
  filtered_issues = @options[:include_labels].nil? ? issues : issues.select { |issue| (issue.labels.map(&:name) & @options[:include_labels]).any? }

  if @options[:add_issues_wo_labels]
    issues_wo_labels = issues.select { |issue|
      !issue.labels.map(&:name).any?
    }
    filtered_issues |= issues_wo_labels
  end
  filtered_issues
end


114
115
116
# File 'lib/github_changelog_generator.rb', line 114

def print_json(json)
  puts JSON.pretty_generate(json)
end