File Coverage

blib/lib/Statocles/Deploy/Git.pm
Criterion Covered Total %
statement 39 40 97.5
branch 2 4 50.0
condition 2 4 50.0
subroutine 8 8 100.0
pod n/a
total 51 56 91.0


line stmt bran cond sub pod time code
1             package Statocles::Deploy::Git;
2             our $VERSION = '0.086';
3             # ABSTRACT: Deploy a site to a Git repository
4              
5 3     3   34369 use Statocles::Base 'Class';
  3         7  
  3         30  
6             extends 'Statocles::Deploy::File';
7              
8 3     3   22594 use Git::Repository;
  3         31885  
  3         12  
9              
10             #pod =attr path
11             #pod
12             #pod The path to deploy to. Must be the root of the Git repository, or a directory
13             #pod inside of the Git repository.
14             #pod
15             #pod =attr branch
16             #pod
17             #pod The Git branch to deploy to. Defaults to "master". If you're building a Github Pages
18             #pod site for a project, you probably want to use the "gh-pages" branch.
19             #pod
20             #pod =cut
21              
22             has branch => (
23             is => 'ro',
24             isa => Str,
25             default => sub { 'master' },
26             );
27              
28             #pod =attr remote
29             #pod
30             #pod The name of the remote to deploy to. Defaults to 'origin'.
31             #pod
32             #pod =cut
33              
34             has remote => (
35             is => 'ro',
36             isa => Str,
37             default => sub { 'origin' },
38             );
39              
40             #pod =method deploy
41             #pod
42             #pod my @paths = $deploy->deploy( $from_store, %options );
43             #pod
44             #pod Deploy the site, copying from the given L<from_store|Statocles::Store>.
45             #pod Returns the paths that were deployed.
46             #pod
47             #pod Possible options are:
48             #pod
49             #pod =over 4
50             #pod
51             #pod =item clean
52             #pod
53             #pod Remove all the current contents of the deploy directory before copying the
54             #pod new content.
55             #pod
56             #pod =item message
57             #pod
58             #pod An optional commit message to use. Defaults to a generic message.
59             #pod
60             #pod =back
61             #pod
62             #pod =cut
63              
64             around 'deploy' => sub {
65             my ( $orig, $self, $from_store, %options ) = @_;
66              
67             my $deploy_dir = $self->path;
68              
69             # Find the repository root
70             my $root = Path::Tiny->new( "$deploy_dir" ); # clone
71             until ( $root->child( '.git' )->exists || $root->is_rootdir ) {
72             $root = $root->parent;
73             }
74             if ( !$root->child( '.git' )->exists ) {
75             die qq{Deploy path "$deploy_dir" is not in a git repository\n};
76             }
77             my $rel_path = $deploy_dir->relative( $root );
78             #; say "Relative: $rel_path";
79              
80             my $git = Git::Repository->new( work_tree => "$root" );
81              
82             my $current_branch = _git_current_branch( $git );
83             if ( !$current_branch ) {
84             die qq{Repository has no branches. Please create a commit before deploying\n};
85             }
86              
87             # Switch to the right branch
88             if ( !_git_has_branch( $git, $self->branch ) ) {
89             # Create a new, orphan branch
90             # Orphan branches were introduced in git 1.7.2
91             $self->site->log->info( sprintf 'Creating deploy branch "%s"', $self->branch );
92             $self->_run( $git, checkout => '--orphan', $self->branch );
93             $self->_run( $git, 'rm', '-r', '-f', '.' );
94             }
95             else {
96             $self->_run( $git, checkout => $self->branch );
97             }
98              
99             if ( $options{ clean } ) {
100             if ( $current_branch eq $self->branch ) {
101             die "--clean on the same branch as deploy will destroy all content. Stopping.\n";
102             }
103             $self->site->log->info( sprintf 'Cleaning old content in branch "%s"', $self->branch );
104             $self->_run( $git, 'rm', '-r', '-f', '.' );
105             delete $options{ clean };
106             }
107              
108             # Copy the files
109             my @files = $self->$orig( $from_store, %options );
110              
111             # Check to see which files were changed
112             # --porcelain was added in 1.7.0
113             my @status_lines = $git->run(
114             status => '--porcelain', '--ignore-submodules', '--untracked-files',
115             );
116              
117             my %in_status;
118             for my $line ( @status_lines ) {
119             my ( $status, $path ) = $line =~ /^\s*(\S+)\s+(.+)$/;
120             $in_status{ $path } = $status;
121             }
122              
123             #; use Data::Dumper;
124             #; say Dumper \%in_status;
125              
126             # Commit the files
127             @files = grep { $in_status{ $_ } }
128             map { Path::Tiny->new( $rel_path, $_ ) }
129             @files;
130              
131             #; say "Committing: " . Dumper \@files;
132             if ( @files ) {
133             $self->site->log->info( sprintf 'Deploying %d changed files', scalar @files );
134             $self->_run( $git, add => @files );
135             $self->_run( $git, commit => -m => $options{message} || "Site update" );
136             }
137             else {
138             $self->site->log->warn( 'No files changed' );
139             }
140              
141             if ( _git_has_remote( $git, $self->remote ) ) {
142             $self->_run( $git, push => $self->remote => $self->branch );
143             }
144             else {
145             $self->site->log->warn(
146             sprintf 'Git remote "%s" does not exist. Not pushing.', $self->remote,
147             );
148             }
149              
150             # Tidy up
151             $self->_run( $git, checkout => $current_branch );
152              
153             return @files;
154             };
155              
156             # Run the given git command on the given git repository, logging the
157             # command for those running in debug mode
158             sub _run {
159 65     65   407 my ( $self, $git, @args ) = @_;
160 65         3324 $self->site->log->debug( "Running git command: " . join " ", @args );
161 65         6346 return _git_run( $git, @args );
162             }
163              
164             sub _git_run {
165 191     191   2264592 my ( $git, @args ) = @_;
166 191         890 my $cmdline = join " ", 'git', @args;
167 191         1194 my $cmd = $git->command( @args );
168 191   50     2877890 my $stdout = join( "\n", readline( $cmd->stdout ) ) // '';
169 191   50     1011531 my $stderr = join( "\n", readline( $cmd->stderr ) ) // '';
170 191         6341 $cmd->close;
171 191         36023 my $exit = $cmd->exit;
172              
173 191 50       1681 if ( $exit ) {
174 0         0 die "git $args[0] exited with $exit\n\n-- CMD --\n$cmdline\n\n-- STDOUT --\n$stdout\n\n-- STDERR --\n$stderr\n";
175             }
176              
177 191         804 return $cmd->exit;
178             }
179              
180             sub _git_current_branch {
181 15     15   70 my ( $git ) = @_;
182 15         95 my @branches = map { s/^\*\s+//; $_ } grep { /^\*/ } $git->run( 'branch' );
  14         123  
  14         105  
  18         224121  
183 15         19307 return $branches[0];
184             }
185              
186             sub _git_has_branch {
187 14     14   81 my ( $git, $branch ) = @_;
188 14         96 return !!grep { $_ eq $branch } map { s/^[\*\s]\s+//; $_ } $git->run( 'branch' );
  18         223  
  18         251217  
  18         112  
189             }
190              
191             sub _git_has_remote {
192 13     13   80 my ( $git, $remote ) = @_;
193 13         93 return !!grep { $_ eq $remote } map { s/^[\*\s]\s+//; $_ } $git->run( 'remote' );
  11         140  
  11         165244  
  11         62  
194             }
195              
196             sub _git_version {
197 3     3   19758 my $output = `git --version`;
198 3         99 my ( $git_version ) = $output =~ /git version (\d+[.]\d+[.]\d+)/;
199 3 50       43 return unless $git_version;
200 3         61 my $v = sprintf '%i.%03i%03i', split /[.]/, $git_version;
201 3         49 return $v;
202             }
203              
204             1;
205              
206             __END__
207              
208             =pod
209              
210             =encoding UTF-8
211              
212             =head1 NAME
213              
214             Statocles::Deploy::Git - Deploy a site to a Git repository
215              
216             =head1 VERSION
217              
218             version 0.086
219              
220             =head1 DESCRIPTION
221              
222             This class allows a site to be deployed to a Git repository.
223              
224             This class consumes L<Statocles::Deploy|Statocles::Deploy>.
225              
226             =head1 ATTRIBUTES
227              
228             =head2 path
229              
230             The path to deploy to. Must be the root of the Git repository, or a directory
231             inside of the Git repository.
232              
233             =head2 branch
234              
235             The Git branch to deploy to. Defaults to "master". If you're building a Github Pages
236             site for a project, you probably want to use the "gh-pages" branch.
237              
238             =head2 remote
239              
240             The name of the remote to deploy to. Defaults to 'origin'.
241              
242             =head1 METHODS
243              
244             =head2 deploy
245              
246             my @paths = $deploy->deploy( $from_store, %options );
247              
248             Deploy the site, copying from the given L<from_store|Statocles::Store>.
249             Returns the paths that were deployed.
250              
251             Possible options are:
252              
253             =over 4
254              
255             =item clean
256              
257             Remove all the current contents of the deploy directory before copying the
258             new content.
259              
260             =item message
261              
262             An optional commit message to use. Defaults to a generic message.
263              
264             =back
265              
266             =head1 SEE ALSO
267              
268             =over 4
269              
270             =item L<Statocles::Deploy>
271              
272             =back
273              
274             =head1 AUTHOR
275              
276             Doug Bell <preaction@cpan.org>
277              
278             =head1 COPYRIGHT AND LICENSE
279              
280             This software is copyright (c) 2016 by Doug Bell.
281              
282             This is free software; you can redistribute it and/or modify it under
283             the same terms as the Perl 5 programming language system itself.
284              
285             =cut