Skip to content

Add option --configure-sections, --add-sections, --include-merged #587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

eputnam
Copy link
Contributor

@eputnam eputnam commented Nov 7, 2017

There's a lot in this PR.

  • Added a Section class to more easily make the other changes and
    hopefully add flexibility for the future
  • Added an option called configure_sections that allows you create
    your own custom sections. It blows away all other sections and uses
    only the ones you give it.
  • Added an option called add_sections that allows you to add_sections
    to the default section set
  • Added an option called include_merged that can be used when
    configure_sections is defined. Configure sections blows away any and
    all default sections so to get this one back, you have to set this
    option.
  • Added tests for this stuff

@HAIL9000 was a co-author. Because of a little git snafu, I accidentally
squashed all of our work into one so it looks like it was just me.

@eputnam eputnam force-pushed the config_sections branch 3 times, most recently from 04e7624 to 675b032 Compare November 7, 2017 06:12
@HAIL9000
Copy link

HAIL9000 commented Nov 7, 2017

This looks so good! Thank you for running with it. Should we add our new options to the docs...?

@olleolleolle
Copy link
Collaborator

This is a big PR. It may take a few turns of code review to get into the codebase, but don't be discouraged by that.

@HAIL9000 is right, there are a few places where all configuration options are to be placed. That is, duplicated in different formats.

@olleolleolle
Copy link
Collaborator

There are a few style details to polish (later) in this code. RuboCop has its set of complaints.

If we can get more use out of the class Section, I'd be happy. It seems a very good addition to the codebase. Just looking quickly, perhaps a query method Section#merged? would help in some places?

Could the presence of the class Section signal the need for a Release, as well?

@eputnam
Copy link
Contributor Author

eputnam commented Nov 16, 2017

@olleolleolle can you elaborate a little on both of those? i'm not sure i see the need for the merged? function or how the signaling for a release would work.

@hunner
Copy link
Contributor

hunner commented Dec 1, 2017

I have fixed the rubocop failures and reorganized code relating to generating specific releases and sections of each release out into their own classes outside of the main generation code. The methods otherwise remain mostly the same (a few naming tweaks because "section" and "log" were overloaded terms).

This is still a Work In Progress as I would like to add more tests.

# @param [Hash, nil] older_tag Older tag, used for the links. Could be nil for last tag.
# @return [String] Ready and parsed section
def create_entry_for_tag(pull_requests, issues, newer_tag, older_tag = nil)
newer_tag_link, newer_tag_name, newer_tag_time = detect_link_tag_time(newer_tag)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should an entry be told what the tags link/name/time is, or should it be able to figure it out just given a tag?

If the former, this logic moves up to the generator. If the latter, the methods for tag munging need to move into the entry class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latter would be the better design decision, but get_time_of_tag is called by detect_link_tag_time and used in multiple other places, so moving detect_link_tag_time to the Entry class would cause issues around that.

Separating the tag utility methods out into their own GeneratorTags module instead of being part of either the Generator or Entry classes would be preferable, but is beyond the scope of this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newer_tag values of name/link/time should be consistently handled in that order inside the Entry class. I have done some small refactoring to make this so.

# the SHA for the first commit.
older_tag_name =
if older_tag.nil?
@fetcher.commits_before(newer_tag_time).last["sha"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should an entry fetch it's preceding commits, or is this logic part of the larger changelog generation?

If the former, then the entry class needs access to the fetcher (pass through options?). If the latter, then this needs to move out of the entry class back to the generator class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create_entry_for_tag method should not make calls out to github, so instead of having the fetcher passed through in the case of a nil older tag, the older tag name should be a required parameter and the generator class should either pass the older tag name or the first commit sha in the absence of an older tag.

The older_tag_link variable used in generate_header is misnamed since it is actually just a tag link, not a name. It is also possibly a sha and not a tag, but I'll ignore that for now.


@content
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All following methods should be private, and create_entry_for_tag should be tested.

content = ""

if issues.any?
content += "#{prefix}\n\n" unless @options[:simple_list]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @options hash is scattered around the various classes and passed around as needed but never modified once parsed. It would be nice if the options were centrally accessible instead of passing them around, but that is out of scope for this PR.

Instead, I'll just make simple_list an invocation option to be removed later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, there are several more options needed by the Section class. I guess I'll pass @options through and it can be refactored in another PR.

@hunner hunner force-pushed the config_sections branch 2 times, most recently from a324d63 to e812a57 Compare December 4, 2017 22:31
@hunner
Copy link
Contributor

hunner commented Dec 4, 2017

@olleolleolle Green and ready for your review again \o/.

@hunner hunner force-pushed the config_sections branch 2 times, most recently from 0dbdaa7 to 3e490fe Compare December 4, 2017 22:50
@eputnam
Copy link
Contributor Author

eputnam commented Dec 5, 2017

tagging @kvolkovich-sc since you seem to have some interest in the feature. feel free to have a look at what we've put together!

@skywinder
Copy link
Member

Very powerful PR. Thank you all for your efforts, @eputnam @hunner 🎉

Copy link
Collaborator

@olleolleolle olleolleolle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking at this the other morning, from the top of the changes, making small notes as I went, if you find the time, perhaps you can address these. If not, that's fine, too.

map[section.name] = section
end

map
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

each_with_object opportunity here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

# @param [Time] newer_tag_time - time, when newer tag created
# @param [String] older_tag_name - tag name, used for links.
# @param [String] project_url - url for current project.
# @return [String] - Generate one ready-to-add section.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally tiny thing: the return value should be described as a noun not a verb.

# Generates complete body text for a tag (without a header)
#
# @param [Array] issues
# @param [Array] pull_requests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two, they could be in the order the positional parameters are.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I updated the methods to take the order pull_requests, issues as well for consistency.

content += section.generate_content
end

content
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another each_with_object opportunity

# Boolean method for whether the user is using configure_sections
def configure_sections?
!@options[:configure_sections].nil? && !@options[:configure_sections].empty?
end
Copy link
Collaborator

@olleolleolle olleolleolle Dec 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want you can move these methods to the Options class. It looks like a Hash but it is a class.

]
end

# This method sort issues by types
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo sort -> sorts

end
end
added_pull_requests.each { |p| pull_requests.delete(p) }
@sections
Copy link
Collaborator

@olleolleolle olleolleolle Dec 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local variable name p, could you make it something else?

https://ruby-doc.org/core-2.4.2/Kernel.html#method-i-p is a method, and some folks' eyes read that p as a method rather than as a local variable.

def generate_body(pull_requests, issues)
body = ""
body += main_sections_to_log(pull_requests, issues)
body += merged_section_to_log(pull_requests) if (@options[:pulls] && @options[:add_pr_wo_labels]) || (@options.configure_sections? && @options[:include_merged])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the logic here is not correct. :include_merged defaults to true, but seems like it should default to false. Additionally, the :configure_section option does not override the :add_pr_wo_labels option as it appears to be meant to do.

The UX here needs a little thinking through.

@eputnam eputnam force-pushed the config_sections branch 2 times, most recently from 83bd3c7 to 04420ea Compare December 7, 2017 22:50
@hunner
Copy link
Contributor

hunner commented Dec 8, 2017

Okay, it looks like the add_pr_wo_labels option applies to normal generation and custom section generation now. No include_merged option needed.

@eputnam
Copy link
Contributor Author

eputnam commented Dec 8, 2017

ready for you again, @olleolleolle @skywinder

Copy link
Collaborator

@olleolleolle olleolleolle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read this, and made more notes.

Thanks for keeping up with all this.

github_api: :github_endpoint
github_api: :github_endpoint,
configure_sections: :configure_sections,
add_sections: :add_sections
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are only needed here if they aren't the same - as you can see, all the other pairs are about irregular names.

So, do not add the new option names here.

@@ -0,0 +1,351 @@
# frozen_string_literal: true

describe GitHubChangelogGenerator::Entry do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the form RSpec.describe (not describe) for the outermost describe.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can structure tests inside a module, like this:

module GitHubChangelogGenerator
  RSpec.describe Entry do
    # ...in here, names will be in the namespace of the module.
    # so that you can use shorter names.
  end
end

end
end
describe "#parse_sections" do
before :each do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:each is deprecated RSpec syntax.

That setting is today called :example, but it is also the default.

So you can say before do ... end instead.

expect(sections_array[i].labels).to eq sections_json.first[1]["labels"]
expect(sections_array[i].issues).to eq []
sections_json.shift
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This place is a candidate for using aggregate_failures, the RSpec feature.

There are many expects in the same it here, and if there's a failure, things stop executing.

Look that feature up, it's quite cool.

expect(sections_array[i].labels).to eq sections_hash.first[1][:labels]
expect(sections_array[i].issues).to eq []
sections_hash.shift
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, here's another it with many expects.

}
end

def get_titles(issues)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we change this name to be more about the result of the call?

Example method name:

  • titles_for(issues)

expect(get_titles(enhancement_section.issues)).to eq(["pr enhance"])
expect(get_titles(bug_section.issues)).to eq(["issue bugaboo"])
expect(get_titles(pull_requests)).to eq(["pr no labels"])
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many expects here, too.

let(:sections_array) do
[
GitHubChangelogGenerator::Section.new(name: "foo", prefix: "foofix", labels: %w[test1 test2]),
GitHubChangelogGenerator::Section.new(name: "bar", prefix: "barfix", labels: %w[test3 test4])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, it ought to be possible to just say "Section.new(...)" here.


def initialize(options = {})
@content = ""
@options = Options.new(options)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we decide that it must be an Options that is passed in here?

@hunner hunner force-pushed the config_sections branch 2 times, most recently from 23e8f34 to 5fefc77 Compare December 14, 2017 18:04
@hunner
Copy link
Contributor

hunner commented Dec 14, 2017

Rebased to resolve conflicts. Anything else you'd like to see?

@olleolleolle
Copy link
Collaborator

@hunner One small thing!

We really could have some doc blocks on these new classes, with more-descriptive words, such as this great section:

I reasoned that a changelog is a series of "entries" of all tagged
releases plus an extra entry for the unreleased entry. Each entry is
comprised of a header and a series of "sections" for that entry. Each
section is comprized of a list of issues and/or pull requests for that
entry. So the log contains entries, entries contain sections, and
sections contain issues & prs. I have structured the classes around this idea.

begin
sections_json = JSON.parse(sections_desc)
rescue JSON::ParserError => e
raise "There was a problem parsing your JSON string for secions: #{e}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: secions => sections

sections_arr << Section.new(name: name.to_s, prefix: v["prefix"], labels: v["labels"], options: @options)
end

sections_arr
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can leave off naming a local variable here, and only return the result of #map

sections_json.map do |name, v|
  Section.new(name: name.to_s, prefix: v["prefix"],
              labels: v["labels"], options: @options)
end 

There's a lot in this PR.
- Added a Section class to more easily make the other changes and
  hopefully add flexibility for the future
- Added an option called `configure_sections` that allows you create
  your own custom sections. It blows away all other sections and uses
only the ones you give it.
- Added an option called `add_sections` that allows you to add_sections
  to the default section set
- Added an option called `include_merged` that can be used when
  configure_sections is defined. Configure sections blows away any and
all default sections so to get this one back, you have to set this
option.
- Added tests for this stuff

@HAIL9000 was a co-author. Because of a little git snafu, I accidentally
squashed all of our work into one so it looks like it was just me.

---

Refactor details:

Before this change, the code in generator.rb and generator_generation.rb was conflated and
method call flow went back and forth between the two files seemingly
randomly. They also both defined the exact same class, which is
un-ruby-ish. I tried to separate methods used for the whole changelog
generation from methods used for specific parts of the changelog and
move them into specific classes.

I reasoned that a changelog is a series of "entries" of all tagged
releases plus an extra entry for the unreleased entry. Each entry is
comprised of a header and a series of "sections" for that entry. Each
section is comprized of a list of issues and/or pull requests for that
entry. So the log contains entries, entries contain sections, and
sections contain issues & prs. I have structured the classes around this idea.

- lib/github_changelog_generator/generator/generator.rb is for code
related to generating the entire changelog.
- lib/github_changelog_generator/generator/entry.rb is for code related
to generating entries.
- lib/github_changelog_generator/generator/section.rb is for code
relating to geneating entry sections.

Issues and PRs are already special objects, so it doesn't make sense to
break those out into their own class.
@olleolleolle olleolleolle changed the title configure custom sections Add option --configure_sections Dec 15, 2017
@olleolleolle olleolleolle changed the title Add option --configure_sections Add option --configure_sections, --add_sections, --include_merged Dec 15, 2017
@olleolleolle olleolleolle changed the title Add option --configure_sections, --add_sections, --include_merged Add option --configure-sections, --add-sections, --include-merged Dec 15, 2017
@olleolleolle olleolleolle merged commit e2ddb73 into github-changelog-generator:master Dec 15, 2017
@olleolleolle
Copy link
Collaborator

It's been a marathon, all!

👍

@hunner
Copy link
Contributor

hunner commented Dec 22, 2017

Thank you for working with us to get this merged too; and thank you for such commitment to maintaining this useful tool!

@hunner hunner deleted the config_sections branch December 22, 2017 03:56
@hunner hunner mentioned this pull request Apr 12, 2018
@skywinder skywinder added this to the 2.0.0 milestone Apr 15, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants