File Coverage

blib/lib/App/git/ship.pm
Criterion Covered Total %
statement 108 165 65.4
branch 24 80 30.0
condition 11 36 30.5
subroutine 23 32 71.8
pod 12 12 100.0
total 178 325 54.7


line stmt bran cond sub pod time code
1             package App::git::ship;
2 11     11   432386 use Mojo::Base -base;
  11         1143226  
  11         110  
3              
4 11     11   1867 use Carp;
  11         23  
  11         790  
5 11     11   71 use Data::Dumper ();
  11         21  
  11         176  
6 11     11   5899 use IPC::Run3 ();
  11         250818  
  11         353  
7 11     11   5483 use Mojo::File 'path';
  11         209582  
  11         732  
8 11     11   5230 use Mojo::Loader;
  11         47110  
  11         550  
9 11     11   5613 use Mojo::Template;
  11         42212  
  11         75  
10 11     11   483 use Mojo::Util qw(decode encode);
  11         33  
  11         652  
11              
12 11   50 11   73 use constant DEBUG => $ENV{GIT_SHIP_DEBUG} || 0;
  11         24  
  11         753  
13 11   100 11   74 use constant SILENT => $ENV{GIT_SHIP_SILENT} || 0;
  11         24  
  11         29491  
14              
15             our $VERSION = '0.35';
16              
17             # Need to be overridden in subclass
18 1     1 1 1429 sub build { $_[0]->abort('build() is not available for %s', ref $_[0]) }
19 1     1 1 1443 sub can_handle_project { $_[0]->abort('can_handle_project() is not available for %s', ref $_[0]) }
20              
21             sub abort {
22 5     5 1 1202 my ($self, $format, @args) = @_;
23 5 100       39 my $message = @args ? sprintf $format, @args : $format;
24              
25 5         19 Carp::confess("!! $message") if DEBUG;
26 5         45 die "!! $message\n";
27             }
28              
29             sub config {
30 17     17 1 3994 my ($self, $key, $value) = @_;
31 17   66     133 my $config = $self->{config} ||= $self->_build_config;
32              
33             # Get all
34 17 100       1855 return $config if @_ == 1;
35              
36             # Get single key
37 16 100       37 if (@_ == 2) {
38 15 100       64 return $config->{$key} if exists $config->{$key};
39              
40 12         32 my $param_method = "_build_config_param_$key";
41 12 100       122 return $self->$param_method if $self->can($param_method);
42              
43 3         19 my $env_key = uc "GIT_SHIP_$key";
44 3   100     66 return decode 'UTF-8', $ENV{$env_key} // '';
45             }
46              
47             # Set single key
48 1         4 $config->{$key} = $value;
49 1         3 return $self;
50             }
51              
52             sub detect {
53 0     0 1 0 my ($self, $file) = (@_, '');
54              
55 0 0       0 if (my $class = $self->config('class')) {
56 0 0       0 $self->abort("Could not load $class: $@") unless eval "require $class;1";
57 0         0 return $class;
58             }
59              
60 0         0 require Module::Find;
61 0         0 for my $class (sort { length $b <=> length $a } Module::Find::findallmod(__PACKAGE__)) {
  0         0  
62 0 0       0 eval "require $class;1" or next;
63 0 0       0 next unless $class->can('can_handle_project');
64 0         0 warn "[ship::detect] $class->can_handle_project($file)\n" if DEBUG;
65 0 0       0 return $class if $class->can_handle_project($file);
66             }
67              
68 0         0 $self->abort("Could not figure out what kind of project this is from '$file'");
69             }
70              
71             sub dump {
72 0     0 1 0 return Data::Dumper->new([$_[1]])->Indent(1)->Terse(1)->Sortkeys(1)->Dump;
73             }
74              
75             sub new {
76 3     3 1 428 my $self = shift->SUPER::new(@_);
77 3         124 open $self->{STDOUT}, '>&STDOUT';
78 3         82 open $self->{STDERR}, '>&STDERR';
79 3         16 return $self;
80             }
81              
82             sub render_template {
83 1     1 1 2069 my ($self, $name, $args) = @_;
84 1 50       5 my $template = $self->_get_template($name) or $self->abort("Could not find template for $name");
85              
86             # Render to string
87 1 50       460 return $template->process({%$args, ship => $self}) if $args->{to_string};
88              
89             # Render to file
90 0         0 my $file = path split '/', $name;
91 0 0 0     0 if (-e $file and !$args->{force}) {
92 0         0 say "# $file exists" unless SILENT;
93 0         0 return $self;
94             }
95              
96             # Try to read template from $HOME/$name
97 0 0       0 if ($args->{template_from_home}) {
98 0 0 0     0 my $src = $ENV{HOME} ? path $ENV{HOME}, $name : undef $template->parse($file->slurp)
    0          
99             if $file and -r $file;
100             }
101              
102 0 0       0 $file->dirname->make_path unless -d $file->dirname;
103 0         0 $file->spurt(encode 'UTF-8', $template->process({%$args, ship => $self}));
104 0         0 say "# Generated $file" unless SILENT;
105 0         0 return $self;
106             }
107              
108             sub run_hook {
109 0     0 1 0 my ($self, $name) = @_;
110 0 0       0 my $cmd = $self->config($name) or return;
111 0         0 $self->system($cmd);
112             }
113              
114             sub ship {
115 0     0 1 0 my $self = shift;
116 0         0 my ($branch) = qx(git branch --no-color) =~ /\* (.+)$/m;
117 0         0 my ($remote) = qx(git remote -v) =~ /^origin\s+(.+)\s+\(push\)$/m;
118              
119 0 0       0 $self->abort("Cannot ship without a current branch") unless $branch;
120 0 0       0 $self->abort("Cannot ship without a version number") unless $self->config('next_version');
121 0 0       0 $self->system(qw(git push origin), $branch) if $remote;
122 0         0 $self->system(qw(git tag) => $self->config('next_version'));
123 0 0       0 $self->system(qw(git push --tags origin)) if $remote;
124             }
125              
126             sub start {
127 0     0 1 0 my $self = shift;
128              
129 0 0 0     0 if (@_ and ref($self) eq __PACKAGE__) {
130 0         0 return $self->detect($_[0])->new($self)->start(@_);
131             }
132              
133 0 0 0     0 $self->system(qw(git init-db)) unless -d '.git' and @_;
134 0         0 $self->render_template('.gitignore');
135 0         0 $self->system(qw(git add .));
136 0 0       0 $self->system(qw(git commit -a -m), "git ship start") if @_;
137 0         0 $self;
138             }
139              
140             sub system {
141 1     1 1 567 my ($self, $program, @args) = @_;
142 1         4 my @fh = (undef);
143 1         7 my $exit_code;
144              
145 1         3 if (SILENT) {
146 1         14 my $output = '';
147 1         8 push @fh, (\$output, \$output);
148             }
149             else {
150             my $log = "$program @args";
151             $log =~ s!\n\r?!\\n!g;
152             say "\$ $log";
153             }
154              
155 1         27 warn "[ship]\$ $program @args\n" if DEBUG == 2;
156 1 50       7 IPC::Run3::run3(@args ? [$program => @args] : $program, @fh);
157 1         28 $exit_code = $? >> 8;
158 1 50       4397 return $self unless $exit_code;
159              
160 1         26 if (SILENT) {
161 1         14 chomp $fh[1];
162 1         46 $self->abort("'$program @args' failed: $exit_code (${$fh[1]})");
  0         0  
163             }
164             else {
165             $self->abort("'$program @args' failed: $exit_code");
166             }
167             }
168              
169             sub _build_config {
170 3     3   7 my $self = shift;
171              
172 3   100     23 my $file = $ENV{GIT_SHIP_CONFIG} || '.ship.conf';
173 3         7 my $config = {};
174 3 100   3   19 return $config unless open my $CFG, '<:encoding(UTF-8)', $file;
  3         6  
  3         20  
  3         97  
175              
176 1         1203 while (<$CFG>) {
177 3         23 chomp;
178 3         4 warn "[ship::config] $_\n" if DEBUG == 2;
179 3 50       18 m/\A\s*(?:\#|$)/ and next; # comments
180 3         19 s/\s+(?
181 3 50       19 m/^\s*([^\=\s][^\=]*?)\s*=\s*(.*)$/ or next;
182 3         11 my ($k, $v) = ($1, $2);
183 3         10 $v =~ s!\s+$!!g;
184 3 50       9 next unless length $v;
185 3         10 $config->{$k} = $v;
186 3         10 $config->{$k} =~ s!\\\#!#!g;
187 3         21 warn "[ship::config] $1 = $2\n" if DEBUG;
188             }
189              
190 1         27 return $config;
191             }
192              
193             sub _build_config_param_author {
194 0     0   0 my $self = shift;
195 0   0     0 my $format = shift || '%an <%ae>';
196              
197 0 0       0 open my $GIT, '-|', qw(git log), "--format=$format"
198             or $self->abort("git log --format=$format: $!");
199 0         0 my $author = decode 'UTF-8', readline $GIT;
200 0 0       0 $self->abort("Could not find any author in git log") unless $author;
201 0         0 chomp $author;
202 0         0 warn "[ship::author] $format = $author\n" if DEBUG;
203 0         0 return $author;
204             }
205              
206             sub _build_config_param_bugtracker {
207             return $ENV{GIT_SHIP_BUGTRACKER}
208 0   0 0   0 || join('/', shift->config('homepage'), 'issues') =~ s!(\w)//!$1/!r;
209             }
210              
211             sub _build_config_param_homepage {
212 0   0 0   0 return $ENV{GIT_SHIP_HOMEPAGE} || shift->config('repository') =~ s!\.git$!!r;
213             }
214              
215 0 0   0   0 sub _build_config_param_license { $ENV{GIT_SHIP_LICENSE} || 'artistic_2' }
216 1 50   1   40 sub _build_config_param_project_name { $ENV{GIT_SHIP_PROJECT_NAME} || 'unknown' }
217              
218             sub _build_config_param_repository {
219 1     1   2 my $self = shift;
220 1         2 my $repository;
221              
222 1 50       2753 open my $REPOSITORIES, '-|', qw(git remote -v) or $self->abort("git remote -v: $!");
223 1         1534 while (<$REPOSITORIES>) {
224 0 0       0 next unless /^origin\s+(\S+).*push/;
225 0         0 $repository = $1;
226 0         0 last;
227             }
228              
229             $repository ||= lc sprintf 'https://github.com/%s/%s',
230 1   50     53 $self->config('username') || $ENV{GITHUB_USERNAME} || scalar(getpwuid $<),
      33        
231             $self->config('project_name') =~ s!::!-!gr;
232 1 50       19 $repository =~ s!^[^:]+:!https://github.com/! unless $repository =~ /^http/;
233 1         2 warn "[ship::repository] $repository\n" if DEBUG;
234              
235 1         50 return $repository;
236             }
237              
238             sub _get_template {
239 1     1   3 my ($self, $name) = @_;
240              
241 1         4 my $class = ref $self;
242 1         2 my $str;
243 11     11   101 no strict 'refs';
  11         28  
  11         2412  
244 1         2 for my $package ($class, @{"$class\::ISA"}) {
  1         7  
245 1 50       7 $str = Mojo::Loader::data_section($package, $name) or next;
246 1         594 $name = "$package/$name";
247 1         3 last;
248             }
249              
250 1 50       13 return $str ? Mojo::Template->new->name($name)->vars(1)->parse($str) : undef;
251             }
252              
253             1;
254              
255             =encoding utf8
256              
257             =head1 NAME
258              
259             App::git::ship - Git command for shipping your project
260              
261             =head1 VERSION
262              
263             0.35
264              
265             =head1 SYNOPSIS
266              
267             See L for how to build Perl projects.
268              
269             Below is a list of useful git aliases:
270              
271             # git build
272             $ git config --global alias.build 'ship build'
273              
274             # git cl
275             $ git config --global alias.cl 'ship clean'
276              
277             # git start
278             # git start My/Project.pm
279             $ git config --global alias.start 'ship start'
280              
281             =head1 DESCRIPTION
282              
283             L is a L command for building and
284             shipping your project.
285              
286             The main focus is to automate away the boring steps, but at the same time not
287             get in your (or any random contributor's) way. Problems should be solved with
288             sane defaults according to standard rules instead of enforcing more rules.
289              
290             L differs from other tools like L by I
291             requiring any configuration except for a file containing the credentials for
292             uploading to CPAN.
293              
294             =head2 Supported project types
295              
296             Currently, only L is supported.
297              
298             =head1 ENVIRONMENT VARIABLES
299              
300             Environment variables can also be set in a config file named C<.ship.conf>, in
301             the root of the project directory. The format is:
302              
303             # some comment
304             bugtracker = whatever
305             new_version_format = %v %Y-%m-%dT%H:%M:%S%z
306              
307             Any of the keys are the lower case version of L, but
308             without the "GIT_SHIP_" prefix.
309              
310             Note however that all environment variables are optional, and in many cases
311             L will simply do the right thing, without any configuration.
312              
313             =head2 GIT_SHIP_AFTER_SHIP
314              
315             It is possible to add hooks. These hooks are
316             programs that runs in your shell. Example hooks:
317              
318             GIT_SHIP_AFTER_SHIP="bash script/new-release.sh"
319             GIT_SHIP_AFTER_BUILD="rm -r lib/My/App/templates lib/My/App/public"
320             GIT_SHIP_AFTER_SHIP="cat Changes | mail -s "Changes for My::App" all@my-app.com"
321              
322             =head2 GIT_SHIP_AFTER_BUILD
323              
324             See L.
325              
326             =head2 GIT_SHIP_BEFORE_BUILD
327              
328             See L.
329              
330             =head2 GIT_SHIP_BEFORE_SHIP
331              
332             See L.
333              
334             =head2 GIT_SHIP_BUGTRACKER
335              
336             URL to the bugtracker for this project.
337              
338             =head2 GIT_SHIP_CLASS
339              
340             This class is used to build the object that runs all the actions on your
341             project. This is autodetected by looking at the structure and files in
342             your project. For now this value can be L or
343             L, but any customization is allowed.
344              
345             =head2 GIT_SHIP_CONTRIBUTORS
346              
347             Comma-separated list with C<< name >> of the contributors to this project.
348              
349             =head2 GIT_SHIP_DEBUG
350              
351             Setting this variable will make "git ship" output more information.
352              
353             =head2 GIT_SHIP_HOMEPAGE
354              
355             URL to the home page for this project.
356              
357             =head2 GIT_SHIP_LICENSE
358              
359             The name of the license to use. Defaults to "artistic_2".
360              
361             =head2 GIT_SHIP_SILENT
362              
363             Setting this variable will make "git ship" output less information.
364              
365             =head1 METHODS
366              
367             These methods are interesting in case you want to extend L with
368             your own functionality. L does exactly this.
369              
370             =head2 abort
371              
372             $ship->abort($str);
373             $ship->abort($format, @args);
374              
375             Will abort the application run with an error message.
376              
377             =head2 build
378              
379             $ship->build;
380              
381             This method builds the project. The default behavior is to L.
382             Needs to be overridden in the subclass.
383              
384             =head2 can_handle_project
385              
386             $bool = $class->can_handle_project($file);
387              
388             This method is called by L and should return boolean
389             true if this module can handle the given git project.
390              
391             This is a class method which gets a file as input to detect or have to
392             auto-detect from current working directory.
393              
394             All the modules in the L namespace will be loaded and asked if
395             they can handle the given project you are in or trying to create.
396              
397             =head2 config
398              
399             $hash_ref = $ship->config;
400             $str = $ship->config($name);
401             $self = $ship->config($name => $value);
402              
403             Holds the configuration from end user. The config is by default read from
404             C<.ship.conf> in the root of your project if such a file exists.
405             L can also be used to build the config, but the
406             settings in C<.ship.conf> has priority.
407              
408             =head2 detect
409              
410             $class = $ship->detect;
411             $class = $ship->detect($file);
412              
413             Will detect the sub class in the L namespace which can be
414             used to handle a project. Will first check L or call
415             L on all the classes in the L
416             namespace if not.
417              
418             =head2 dump
419              
420             $str = $ship->dump($any);
421              
422             Will serialize C<$any> into a perl data structure, using L.
423              
424             =head2 new
425              
426             $ship = App::git::ship->new(\%attributes);
427              
428             Creates a new instance of C<$class>.
429              
430             =head2 render_template
431              
432             $ship->render_template($file, \%args);
433              
434             Used to render a template by the name C<$file> to a C<$file>. The template
435             needs to be defined in the C section of the current class or one of
436             the super classes.
437              
438             =head2 run_hook
439              
440             $ship->run_hook($name);
441              
442             Used to run a hook before or after an event. The hook is a command which needs
443             to be defined in L. See also L,
444             L, L and
445             L.
446              
447             =head2 ship
448              
449             $ship->ship;
450              
451             This method ships the project to some online repository. The default behavior
452             is to make a new tag and push it to "origin". Push occurs only if origin is
453             defined in git.
454              
455             =head2 start
456              
457             $ship->start;
458              
459             This method is called when initializing the project. The default behavior is
460             to populate L with default data:
461              
462             =head2 system
463              
464             $ship->system($program, @args);
465              
466             Same as perl's C, but provides error handling and logging.
467              
468             =head1 SEE ALSO
469              
470             =over
471              
472             =item * L
473              
474             This project can probably get you to the moon.
475              
476             =item * L
477              
478             This looks really nice for shipping your project. It has the same idea as
479             this distribution: Guess as much as possible.
480              
481             =item * L
482              
483             One magical tool for doing it all in one bang.
484              
485             =back
486              
487             =head1 COPYRIGHT AND LICENSE
488              
489             Copyright (C) 2014-2018, Jan Henning Thorsen
490              
491             This program is free software, you can redistribute it and/or modify it under
492             the terms of the Artistic License version 2.0.
493              
494             =head1 AUTHOR
495              
496             Jan Henning Thorsen - C
497              
498             mohawk2 - C
499              
500             Rolf Stöckli - C
501              
502             Shoichi Kaji - C
503              
504             =cut
505              
506             __DATA__