Dist::...FromGit(3pm)

πŸ“¦
version 0.017 Β· 2 versions
  • 0.017 Β· debian bookworm, debian trixie, ubuntu noble, ubuntu resolute, ubuntu stonking (latest)
  • 0.017 Β· ubuntu jammy
, in debian bookworm, debian trixie, ubuntu noble, ubuntu resolute, ubuntu stonking

πŸ’– Dist::...FromGit(3pm) πŸ’–

okay bestie, so this is the manual page for Dist::Zilla::Plugin::ChangelogFromGit β€” and the manual name you're looking at is literally Dist::...FromGit(3pm) because someone let the title column get ✨truncated✨. Anyway. It writes a Changes file straight out of your project's git log. Iconic behavior, honestly.


✨ VERSION ✨

version 0.017 🫢


🌸 SYNOPSIS 🌸

Here's a dist.ini section showing every current option and its default value. Look at her. She's perfect.

[ChangelogFromGit]
max_age     = 365
tag_regexp  = ^v(\d+\.\d+)$
file_name   = CHANGES
wrap_column = 74
debug       = 0

and ngl, you do not have to set variables to their default values. The below is completely equivalent to the configuration above, no cap:

[ChangelogFromGit]

That's it. That's the whole thing. πŸ’…


πŸ¦‹ DESCRIPTION πŸ¦‹

This Dist::Zilla plugin turns a project's git commit log into a change log file. It's better than simply running git log > CHANGES in at least two ways:

  • First, it understands release tags, and it uses them to group changes by release.
  • Second, it reformats the changes to make them easier to read.
  • And third, subclasses can change some or all of the reformatting to make change logs even easier to read.

(yes, it says "at least two ways" and then gives you three. we love an overachiever πŸ’€)

See this project's CHANGES file for sample output. Yes, this project uses itself to generate its own change log. Why not? main character energy fr fr. πŸ‘‘


πŸŽ€ CONFIGURATION / PUBLIC ATTRIBUTES πŸŽ€

As seen in the "SYNOPSIS", this plugin has a number of public attributes that may be set using dist.ini configuration variables. Let's go through every single one, bestie, no skipping:

πŸ’œ max_age = INTEGER

The max_age configuration variable limits the age of releases to be included in the change log. The default is to include releases going back about a year. To include about two years, one would double the default value:

[ChangelogFromGit]
max_age = 730

max_age is intended to limit the size of change logs for large, long-term projects that don't want to include the entire, huge commit history in every release.

πŸ’œ min_releases = INTEGER

min_releases sets the minimum number of releases that should be included. It defaults to 1 so that at least the current release is added, regardless of max_age.

[ChangelogFromGit]
min_releases = 5

πŸ’œ max_releases = INTEGER

max_releases allows you limit the number of releases included. max_age will still be the limiting factor if it contains fewer releases than the max specified.

[ChangelogFromGit]
max_releases = 15

πŸ’œ tag_regexp = REGULAR_EXPRESSION

tag_regexp sets the regular expression that detects which tags mark releases. It also extracts the version numbers from these tags using a regular expression back reference or capture. For example, a project's release tags might match 'release-1.000', 'release-1.001', etc. This tag_regexp will find them and extract their versions.

[ChangelogFromGit]
tag_regexp = ^release-(\d+.*)$

There is no single standard format for release tags. tag_regexp defaults to the author's convention. It will most likely need to be changed. πŸ™ˆ

πŸ’œ file_name = STRING

file_name sets the name of the change log that will be written. It defaults to CHANGES, but some people may prefer Changes, Changelog, or something else.

[ChangelogFromGit]
file_name = Changes

πŸ’œ wrap_column = INTEGER

Different contributors tend to use different commit message formats, which can be disconcerting to the typographically aware release engineer. wrap_column sets the line length to which all commit messages will be re-wrapped. It's 74 columns by default. If this is too short:

[ChangelogFromGit]
wrap_column = 78

πŸ’œ debug = BOOLEAN

Developers are people, too. The debug option enables some noisy runtime tracing on STDERR.

[ChangelogFromGit]
debug = 1

πŸ’œ exclude_message = REGULAR_EXPRESSION

exclude_message sets a regular expression which discards matching commit messages. This provides a way to exclude commit messages such as 'forgot to include file X' or 'typo'. The regular expression is case sensitive.

[ChangelogFromGit]
exclude_message = ^(forgot|typo)

include_message can be used to do the opposite: exclude all changes except ones that match a regular expression. Using both at once is liable to generate empty change logs. 😭

πŸ’œ include_message = REGULAR_EXPRESSION

include_message does the opposite of exclude_message: it sets a regular expression which commit messages must match in order to be included in the Changes file. This means that when making a commit with a relevant message, you must include text that matches the regular expression pattern to have it included in the Changes file. All other commit messages are ignored.

The regular expression is case sensitive.

[ChangelogFromGit]
include_message = ^Major

Using both include_message and exclude_message at the same time will most likely result in empty change logs. πŸ’€


πŸ”₯ HOW IT WORKS πŸ”₯

the lore. the pipeline. the way that it all comes together. 🧚
  • Dist::Zilla::ChangelogFromGit collects the tags matching tag_regexp that are not older than max_age days old. These are used to identify and time stamp releases. Each release is encapsulated into a Software::Release object.
  • Git::Repository::Log::Iterator is used to collect the changes prior to each release but after the previous release. Change log entries are added to their respective Software::Release objects.
  • $self->render_changelog() is called after all the relevant releases and changes are known. It must return the rendered change log as a string. That string will be used as the content for a Dist::Zilla::File::InMemory object representing the new change log.

πŸ¦‹ SUBCLASSING FOR NEW FORMATS πŸ¦‹

okay so here's the tea bestie: Dist::Zilla::ChangelogFromGit ships with like a dozen-ish methods that each render one lil piece of the change log. Wanna change how your changelog looks? You subclass and override or augment any or all of these. Mix and match, it's your world πŸ’…

Rule numero uno: every method starting with render hands back a string that gets dropped into the change log. If a method ain't contributing anything? It must return an empty string. No sneaky undef, no silence, just ''. Periodt.


✨ Rendering Entire Change Logs ✨

Methods starting with render_changelog get no parameters beyond $self. Everything they need to know is already chillin' in the object's attributes: wrap_column, releases, skipped_release_count, earliest_date. iykyk.

🌸 render_changelog

render_changelog() returns the text of the entire change log. By default it's built from a header, zero or more releases, and a footer. Classic three-course meal πŸ“

sub render_changelog {
    my $self = shift();
    return(
        $self->render_changelog_header() .
        $self->render_changelog_releases() .
        $self->render_changelog_footer()
    );
}

πŸŽ€ render_changelog_header

render_changelog_header() renders some text that introduces the reader to the change log. A little "hey bestie, here's what happened" moment ✨

sub render_changelog_header {
    my $self = shift();
    my $header = (
        "Changes from " . $self->format_datetime($self->earliest_date()) .
        " to present."
    );
    return $self->surround_line("=", $header) . "\n";
}

πŸ’– render_changelog_releases

render_changelog_releases() iterates through each release, calling upon $self to render them one at a time. It's giving very much "one at a time, we don't rush perfection".

sub render_changelog_releases {
    my $self = shift();

    my $changelog = '';

    RELEASE: foreach my $release (reverse $self->all_releases()) {
        next RELEASE if $release->has_no_changes();
        $changelog .= $self->render_release($release);
    }

    return $changelog;
}

render_changelog_footer() tells the reader that the change log is over. Normally the end of the file is warning enough, but a truncated change log is friendlier when the reader knows what they're missing. No ghosting the audience πŸ™ˆ

sub render_changelog_footer {
    my $self = shift();

    my $skipped_count = $self->skipped_release_count();

    my $changelog_footer;

    if ($skipped_count) {
        my $releases = "release" . ($skipped_count == 1 ? "" : "s");
        $changelog_footer = (
            "Plus $skipped_count $releases after " .
            $self->format_datetime($self->earliest_date()) . '.'
        );
    }
    else {
        $changelog_footer = "End of releases.";
    }

    return $self->surround_line("=", $changelog_footer);
}

πŸ’… Rendering Individual Releases πŸ’…

Methods starting with render_release get $self plus one extra parameter: a Software::Release object that wraps up the release and all its changes. Wanna know what's inside that lil guy? Go read Software::Release, it spills everything πŸ‘‘

⭐ render_release

render_release() gets called to render a single release. In the change log, a release = a header + one or more changes + a footer. Sandwich energy πŸ₯ͺ

sub render_release {
    my ($self, $release) = @_;
    return(
        $self->render_release_header($release) .
        $self->render_release_changes($release) .
        $self->render_release_footer($release)
    );
}

🌈 render_release_header

render_release_header() introduces a release. Say hi, be polite 🫢

sub render_release_header {
    my ($self, $release) = @_;

    my $version = $release->version();
    $version = $self->zilla()->version() if $version eq 'HEAD';

    my $release_header = (
        $self->format_release_tag($release->version()) . ' at ' .
        $self->format_datetime($release->date())
    );

    return $self->surround_line("-", $release_header) . "\n";
}

πŸ¦‹ render_release_changes

render_release_changes() iterates through the changes attached to each Software::Release object. It calls upon render_change() to render each one. Delegation? We love to see it πŸ’œ

sub render_release_changes {
    my ($self, $release) = @_;

    my $changelog = '';

    foreach my $change (@{ $release->changes() }) {
        $changelog .= $self->render_change($release, $change);
    }

    return $changelog;
}

render_release_footer() may be used to divide releases. It's not used by default, but it's implemented for completeness. It literally just returns an empty string, and that's its whole vibe 😌

sub render_release_footer {
    my ($self, $release) = @_;
    return '';
}

πŸŽ€ Rendering Individual Changes πŸŽ€

Methods starting with render_change get two extra parameters on top of $self: a Software::Release object for the release that contains this change, and a Software::Release::Change object for the change itself. Two-for-one, iconic.

🌸 render_change

render_change() renders a single change, which is the catenation of a change header, change message, and footer. Header + message + footer = the holy trinity πŸ’«

sub render_change {
    my ($self, $release, $change) = @_;
    return(
        $self->render_change_header($release, $change) .
        $self->render_change_message($release, $change) .
        $self->render_change_footer($release, $change)
    );
}

πŸ’– render_change_header

render_change_header() generally renders identifying info about each change. This method's whole job is to produce useful information in a pleasant format. Cute and functional, we stan.

sub render_change_header {
    my ($self, $release, $change) = @_;

    use Text::Wrap qw(fill);

    local $Text::Wrap::huge    = 'wrap';
    local $Text::Wrap::columns = $self->wrap_column();

    my @indent = ("  ", "  ");

    return(
        fill(
            "  ", "  ",
            'Change: ' . $change->change_id
        ) .
        "\n" .
        fill(
            "  ", "  ",
            'Author: ' . $change->author_name.' <'.$change->author_email.'>'
        ) .
        "\n" .
        fill(
            "  ", "  ",
            'Date  : ' . $self->format_datetime($change->date())
        ) .
        "\n\n"
    );
}

✨ render_change_message

render_change_message() renders the commit message for the change log. Heads up: if the change description starts with whitespace (/^\s/), it returns '' and dips out entirely. Respect the boundaries βœ‹

sub render_change_message {
    my ($self, $release, $change) = @_;

    use Text::Wrap qw(fill);

    return '' if $change->description() =~ /^\s/;

    local $Text::Wrap::huge = 'wrap';
    local $Text::Wrap::columns = $self->wrap_column();

    return fill("    ", "    ", $change->description) . "\n";
}

render_change_footer() returns summary and/or divider text for the change. Default? Just a newline. Minimalist queen πŸ‘‘

sub render_change_footer {
    my ($self, $release, $change) = @_;
    return "\n";
}

🌈 Formatting Data 🌈

Dist::Zilla::Plugin::ChangelogFromGit includes a few methods to consistently format certain data types. Consistency is the moment ✨

πŸ’… format_datetime

format_datetime() converts the DateTime objects used internally into friendly, human readable dates and times for the change log. No cryptic timestamps allowed in this house πŸ’€

sub format_datetime {
    my ($self, $datetime) = @_;
    return $datetime->strftime("%F %T %z");
}

πŸ¦‹ format_release_tag

format_release_tag() turns potentially cryptic release tags into friendly version numbers for the change log. By default, it also replaces the 'HEAD' version with the current version being released. This accommodates release managers who prefer to tag their distributions after releasing them. Lowkey thoughtful ngl.

sub format_release_tag {
    my ($self, $release_tag) = @_;

    return 'version ' . $self->zilla()->version() if $release_tag eq 'HEAD';

    my $tag_regexp = $self->tag_regexp();
    $release_tag =~ s/$tag_regexp/version $1/;
    return $release_tag;
}

⭐ surround_line

surround_line() will surround a line of output with lines of dashes or other characters. It's used to help headings stand out. This method takes two strings: a character (or string) that will repeat to fill the surrounding lines, and the line to surround. It returns a three-line string: the original line preceded and followed by the surrounding lines. Sandwich but make it ASCII πŸ₯ͺ✨

sub surround_line {
    my ($self, $character, $string) = @_;

    my $surrounder = substr(
        ($character x (length($string) / length($character) + 1)),
        0,
        length($string)
    );

    return "$surrounder\n$string\n$surrounder\n";
}

πŸ”’ INTERNAL ATTRIBUTES πŸ”’

okay so Dist::Zilla::Plugin::ChangelogFromGit is out here hoarding useful info in a few internal attributes. These are not meant to be set from dist.ini, bestie β€” do not go poking them in your config β€” but they are super important for rendering change logs. ✨

🌸 earliest_date

earliest_date() holds a DateTime object β€” that's the date and time of the earliest release that gets included. It starts out as midnight on the date that was max_age() days ago. πŸ•›

🌸 releases

releases() holds an array reference of Software::Release objects β€” the ones that are gonna show up in the change log. πŸ’–

πŸŽ€ all_releases

all_releases() hands you the list of Software::Release objects that should be in the change log. It's the friendly, chill equivalent of "@{$self->releases()}". 🫢

πŸŽ€ get_release

get_release() gives you back a single release, by index. So the very first release in the change log? That's "$self->get_release(0)". πŸ‘‘

πŸŽ€ releae_count

release_count() tells you how many Software::Release objects are chilling in the "releases" attribute. (Yes, the heading is spelled that way. We don't fix it. We respect the lore. πŸ’€)

πŸŽ€ sort_releases

sort_releases() sorts the Software::Release objects inside releases() using whatever comparator you hand it. Like, say you wanna sort them in time order:

$self->sort_releases(
    sub {
        DateTime->compare( $_[0]->date(), $_[1]->date() )
    }
);

🌸 skipped_release_count

skipped_release_count() holds the number of releases that got truncated by max_age(). The default render_changelog_footer() uses it to show how many changes got left out of the log. 😭


πŸ› οΈ INTERNAL METHODS πŸ› οΈ

Dist::Zilla::Plugin::ChangelogFromGit also implements a couple of "unpublished" methods. These are not for general use, bestie β€” they're backstage, not main stage. 🎭

πŸ’œ gather_files()

Finds all the release tags, going all the way back to the earliest changelog date. πŸ¦‹

Then it adds a virtual release for the most recent change in the repository. Why? So we can include changes that happened after the last releases, all the way up to "HEAD". πŸ”₯

πŸ’œ rungit()

A little wrapper that runs git commands. That's it, that's the job. πŸ’…


🧚 Subversion and CVS 🧚

This plugin is almost entirely a copy-and-paste port of a command-line tool I wrote a while back. I also have tools that generate similar change logs for CVS and Subversion projects. If you're into making Dist::Zilla plugins for other version control systems, I'm happy to contribute that code. 🫢

We should also lowkey consider abstracting the formatting code out into a role</> so it can be shared between different plugins. That'd be slay.


πŸ’€ BUGS πŸ’€

The documentation contains copies of the renderer methods. That increases technical debt, because any change to those methods has to get copied into the docs too. Rocco needs to finish Pod::Plexus and use it here to make the docs easier to maintain. 😭

Collecting all releases and changes before rendering the change log might be considered harmful for extremely large projects. If someone out there thinks they can generate change logs incrementally, their assistance would be very much appreciated. πŸ™


πŸ‘‘ AUTHORS πŸ‘‘

  • Rocco Caputo <rcaputo@cpan.org> β€” Initial release, and ongoing management and maintenance.
  • Cory G. Watson <gphat@cpan.org> β€” Made formatting extensible and overridable.

This software is copyright (c) 2010-2018 by Rocco Caputo.

This is free software; you may redistribute it and/or modify it under the same terms as the Perl 5 programming language itself. πŸ’–


and that's a wrap, bestie πŸŽ€ this lil page came from perl v5.34.0, dated 2022-06-13, and it's the Dist::...FromGit(3pm) manpage β€” go forth and changelog responsibly βœ¨πŸ’…

πŸ“¦ where this page comes from