File Coverage

blib/lib/App/Changelog.pm
Criterion Covered Total %
statement 33 79 41.7
branch 9 28 32.1
condition 6 8 75.0
subroutine 6 10 60.0
pod 0 2 0.0
total 54 127 42.5


line stmt bran cond sub pod time code
1             package App::Changelog;
2              
3 2     2   320078 use strict;
  2         3  
  2         78  
4 2     2   6 use warnings;
  2         3  
  2         120  
5              
6 2     2   9 use feature 'say';
  2         10  
  2         2534  
7              
8             our $VERSION = '1.0.2';
9              
10             sub new {
11 3     3 0 186846 my ( $class, %args ) = @_;
12             my $self = {
13             output_file => $args{output_file} || 'CHANGELOG.md',
14             compact => $args{compact} // 1,
15             filter_tag => $args{filter_tag} || '',
16 3   50     62 conventional => $args{conventional} // 0,
      100        
      100        
      50        
17             };
18 3         8 bless $self, $class;
19 3         8 return $self;
20             }
21              
22             sub generate_changelog {
23 0     0 0 0 my ($self) = @_;
24              
25 0         0 say "Generating changelog from Git history...";
26              
27             my $git_log_format =
28 0 0       0 $self->{compact} ? '--pretty=format:"%h %s"' : '--pretty=fuller';
29 0 0       0 if ( $self->{conventional} ) {
30 0         0 $git_log_format = '--pretty=format:"%h %s (%an)"';
31             }
32              
33 0         0 my $git_log =
34             $self->_run_git_command("git log $git_log_format --abbrev-commit");
35 0 0       0 if ( !$git_log ) {
36 0         0 die
37             "Error: Could not retrieve Git history. Are you in a Git repository?\n";
38             }
39              
40 0         0 my @tags = $self->_get_tags();
41 0         0 my $changelog_content =
42             $self->_build_changelog_content( \@tags, $git_log_format );
43              
44 0         0 $self->_write_to_file($changelog_content);
45 0         0 say "Changelog generated successfully in $self->{output_file}.";
46             }
47              
48             sub _build_changelog_content {
49 1     1   9 my ( $self, $tags, $format ) = @_;
50 1         3 my $content = "# Changelog\n\n";
51              
52 1         3 for my $i ( 0 .. $#$tags ) {
53 3         3 my $current_tag = $tags->[$i];
54 3 100       7 my $previous_tag = $i == $#$tags ? '' : $tags->[ $i + 1 ];
55              
56 3 100       7 my $log_command =
57             $previous_tag
58             ? "git log $previous_tag..$current_tag $format"
59             : "git log $current_tag $format";
60              
61 3         4 my $logs = $self->_run_git_command($log_command);
62             $logs = $self->_filter_conventional_commits($logs)
63 3 50       14 if $self->{conventional};
64              
65 3         7 my $date =
66             $self->_run_git_command("git log -1 --format=%ai $current_tag");
67 3         45 $date =~ s/\s.*$//;
68              
69 3         7 $content .= "## [$current_tag] - $date\n\n";
70              
71 3 50       6 unless ( $self->{conventional} ) {
72 3 50       9 $content .= "$logs\n" if $logs;
73             }
74             else {
75 0         0 my %grouped_commits;
76 0         0 my @log_lines = split( "\n", $logs );
77              
78 0         0 for my $log (@log_lines) {
79 0 0       0 if ( $log =~ /^[a-f0-9]+\s([a-z]+):\s*(.*)$/ ) {
80 0         0 my $type = $1;
81 0         0 my $message = $2;
82              
83 0         0 push @{ $grouped_commits{$type} }, $message;
  0         0  
84             }
85             }
86              
87 0         0 for my $type ( sort keys %grouped_commits ) {
88 0         0 $content .= "### " . ucfirst($type) . "\n";
89 0         0 for my $message ( @{ $grouped_commits{$type} } ) {
  0         0  
90 0         0 $content .= "- $type: $message\n";
91             }
92 0         0 $content .= "\n";
93             }
94             }
95             }
96              
97 1         5 return $content;
98             }
99              
100             sub _filter_conventional_commits {
101 0     0   0 my ( $self, $logs ) = @_;
102 0         0 my @lines = split( /\n/, $logs );
103 0         0 my @filtered;
104              
105 0         0 foreach my $line (@lines) {
106 0 0       0 if ( $line =~
107             /^feat|fix|chore|docs|style|refactor|test|perf|ci|build|revert\(.*?\): /
108             )
109             {
110 0         0 push @filtered, $line;
111             }
112             }
113              
114 0         0 return join( "\n", @filtered );
115             }
116              
117             sub _get_tags {
118 1     1   4 my ($self) = @_;
119 1         2 my $git_tags = $self->_run_git_command('git tag --sort=creatordate');
120 1         11 my @tags = split( /\n/, $git_tags );
121 1 50       3 if ( !@tags ) {
122 0         0 die
123             "Error: No Git tags found. Use 'git tag' to create version tags first.\n";
124             }
125              
126 1 50       5 if ( $self->{filter_tag} ) {
127 0         0 @tags = grep { /^$self->{filter_tag}/ } @tags;
  0         0  
128 0 0       0 if ( !@tags ) {
129 0         0 die "Error: No tags matching the filter '$self->{filter_tag}'.\n";
130             }
131             }
132 1         3 return @tags;
133             }
134              
135             sub _run_git_command {
136 0     0     my ( $self, $command ) = @_;
137 0           my $output = `$command`;
138 0           chomp $output;
139 0           return $output;
140             }
141              
142             sub _write_to_file {
143 0     0     my ( $self, $content ) = @_;
144             open( my $fh, '>', $self->{output_file} )
145 0 0         or die "Could not open $self->{output_file}: $!";
146 0           print $fh $content;
147 0           close($fh);
148             }
149              
150             1;
151             __END__