File Coverage

blib/lib/Mojolicious/Plugin/Export/Git.pm
Criterion Covered Total %
statement 86 88 97.7
branch 20 30 66.6
condition 11 15 73.3
subroutine 10 10 100.0
pod 1 1 100.0
total 128 144 88.8


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Export::Git;
2             our $VERSION = '0.003';
3             # ABSTRACT: Export a Mojolicious site to a Git repository
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod use Mojolicious::Lite;
8             #pod get '/' => 'index';
9             #pod get '/secret' => 'secret';
10             #pod plugin 'Export::Git' => {
11             #pod pages => [qw( / /secret )],
12             #pod branch => 'gh-pages',
13             #pod };
14             #pod app->start;
15             #pod
16             #pod =head1 DESCRIPTION
17             #pod
18             #pod Deploy a Mojolicious webapp to a Git repository.
19             #pod
20             #pod This plugin requires Git version 1.7.2 (released July 21, 2010) or later.
21             #pod
22             #pod =head1 ATTRIBUTES
23             #pod
24             #pod This class inherits from L and adds the
25             #pod following attributes:
26             #pod
27             #pod =head1 METHODS
28             #pod
29             #pod This class inherits from L and adds the
30             #pod following methods:
31             #pod
32             #pod =head1 SEE ALSO
33             #pod
34             #pod L, L
35             #pod
36             #pod =cut
37              
38 1     1   512470 use Mojo::Base 'Mojolicious::Plugin::Export';
  1         8  
  1         5  
39 1     1   3683 use Mojo::File qw( path );
  1         2  
  1         38  
40 1     1   451 use Git::Repository;
  1         25303  
  1         6  
41              
42             #pod =attr branch
43             #pod
44             #pod The Git branch to deploy to. Defaults to "master". If you're building
45             #pod a Github Pages site for a project, you probably want to use the
46             #pod "gh-pages" branch.
47             #pod
48             #pod =cut
49              
50             has branch => sub { 'master' };
51              
52             #pod =attr remote
53             #pod
54             #pod The name of the remote to deploy to. Defaults to 'origin'.
55             #pod
56             #pod =cut
57              
58             has remote => sub { 'origin' };
59              
60             #pod =attr clean
61             #pod
62             #pod If true, will completely remove all existing files before exporting. This
63             #pod ensures that any deleted files will be deleted from the repository.
64             #pod
65             #pod Defaults to true if L isn't the current branch.
66             #pod
67             #pod =cut
68              
69             has clean => sub { undef };
70              
71             sub export {
72 10     10 1 312715 my ( $self, $opt ) = @_;
73 10         112 for my $key ( qw( to quiet branch remote clean ) ) {
74 50   100     774 $opt->{ $key } //= $self->$key;
75             }
76              
77             # Find the repository root
78 10         76 my $repo_root = path( $opt->{to} );
79 10   100     299 until ( -e $repo_root->child( '.git' ) || $repo_root->to_abs eq '/' ) {
80 3         550 $repo_root = $repo_root->dirname;
81             }
82 10 100       928 if ( !-e $repo_root->child( '.git' ) ) {
83 1         40 die qq{Export path "$opt->{to}" is not in a git repository\n};
84             }
85 9         383 my $deploy_dir = path( $opt->{to} );
86 9         226 my $rel_path = $deploy_dir->to_rel( $repo_root );
87 9         2047 my $git = Git::Repository->new( work_tree => "$repo_root" );
88              
89             # Switch to the right branch for export
90 9         522462 my $current_branch = _git_current_branch( $git );
91 9 100       90 if ( !$current_branch ) {
92 1         45 die qq{Repository has no branches. Please create a commit before deploying\n};
93             }
94 8 100       132 if ( !_git_has_branch( $git, $opt->{branch} ) ) {
95             # Create a new, orphan branch
96             # Orphan branches were introduced in git 1.7.2
97             say sprintf ' [git] Creating deploy branch "%s"', $opt->{branch}
98 1 50       24 unless $opt->{quiet};
99 1         29 $self->_run( $git, checkout => '--orphan', $opt->{branch} );
100 1         217 $self->_run( $git, 'rm', '-r', '-f', '.' );
101 1         113 $opt->{ clean } = 0;
102             }
103             else {
104             say sprintf ' [git] Checkout deploy branch "%s"', $opt->{branch}
105 7 50       58 unless $opt->{quiet};
106 7         140 $self->_run( $git, checkout => $opt->{branch} );
107             }
108              
109 8   66     959 $opt->{ clean } //= $current_branch ne $opt->{branch};
110 8 100       49 if ( $opt->{ clean } ) {
111 2 100       27 if ( $current_branch eq $opt->{branch} ) {
112 1         61 die qq{Using "clean" on the same branch as deploy will destroy all content. Stopping.\n};
113             }
114             say sprintf ' [git] Cleaning old content in branch "%s"', $opt->{branch}
115 1 50       17 unless $opt->{quiet};
116 1         20 $self->_run( $git, 'rm', '-r', '-f', '.' );
117             }
118              
119             # Export the site
120 7         283 $self->SUPER::export( $opt );
121              
122             # Check to see which files were changed
123             # --porcelain was added in 1.7.0
124 7         194141 my @status_lines = $git->run(
125             status => '--porcelain', '--ignore-submodules', '--untracked-files',
126             );
127              
128 7         115795 my %in_status;
129 7         88 for my $line ( @status_lines ) {
130 7         217 my ( $status, $path ) = $line =~ /^\s*(\S+)\s+(.+)$/;
131 7         73 $in_status{ $path } = $status;
132             }
133              
134             # ; use Data::Dumper;
135             # ; say Dumper \%in_status;
136              
137             # Commit the files
138 5         181 my @files = map { $_->[0] }
139 6         1728 grep { -e $_->[1] }
140 7         85 map { [ $_, path( $repo_root, $_ )->to_rel( $rel_path ) ] }
  6         489  
141             keys %in_status;
142              
143             # ; say "Files to commit: " . join "; ", @files;
144 7 100       69 if ( @files ) {
145             say sprintf ' [git] Deploying %d changed files', scalar @files
146 5 50       39 unless $opt->{quiet};
147 5         86 $self->_run( $git, add => @files );
148 5   50     656 $self->_run( $git, commit => -m => $opt->{message} || "Site update" );
149             }
150             else {
151 2 50       39 say sprintf ' [git] No changes to commit' unless $opt->{quiet};
152             }
153              
154 7 50       628 if ( _git_has_remote( $git, $opt->{remote} ) ) {
155 7         264 $self->_run( $git, push => $opt->{remote} => join ':', ($opt->{branch})x2 );
156             }
157             else {
158             say sprintf ' [git] Remote "%s" does not exist. Not pushing.', $opt->{remote}
159 0 0       0 unless $opt->{quiet};
160             }
161              
162             # Tidy up
163 7         1117 $self->_run( $git, checkout => $current_branch );
164             };
165              
166             # Run the given git command on the given git repository, logging the
167             # command for those running in debug mode
168             sub _run {
169 34     34   360 my ( $self, $git, @args ) = @_;
170             # ; $self->_app->log->debug( "Running git command: " . join " ", @args );
171 34         359 return _git_run( $git, @args );
172             }
173              
174             sub _git_run {
175 101     101   1412837 my ( $git, @args ) = @_;
176 101         1160 my $cmdline = join " ", 'git', @args;
177 101         947 my $cmd = $git->command( @args );
178 101   50     1582127 my $stdout = join( "\n", readline( $cmd->stdout ) ) // '';
179 101   50     651789 my $stderr = join( "\n", readline( $cmd->stderr ) ) // '';
180 101         5972 $cmd->close;
181 101         28795 my $exit = $cmd->exit;
182              
183 101 50       2150 if ( $exit ) {
184 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";
185             }
186              
187 101         851 return $cmd->exit;
188             }
189              
190             sub _git_current_branch {
191 9     9   55 my ( $git ) = @_;
192 9         130 my @branches = map { s/^\*\s+//; $_ } grep { /^\*/ } $git->run( 'branch' );
  8         100  
  8         115  
  13         121335  
193 9         18073 return $branches[0];
194             }
195              
196             sub _git_has_branch {
197 8     8   63 my ( $git, $branch ) = @_;
198 8         106 return !!grep { $_ eq $branch } map { s/^[\*\s]\s+//; $_ } $git->run( 'branch' );
  13         235  
  13         124397  
  13         80  
199             }
200              
201             sub _git_has_remote {
202 7     7   105 my ( $git, $remote ) = @_;
203 7         104 return !!grep { $_ eq $remote } map { s/^[\*\s]\s+//; $_ } $git->run( 'remote' );
  7         232  
  7         111810  
  7         61  
204             }
205              
206             sub _git_version {
207 1     1   21111 my $output = `git --version`;
208 1         97 my ( $git_version ) = $output =~ /git version (\d+[.]\d+[.]\d+)/;
209 1 50       33 return unless $git_version;
210 1         34 my $v = sprintf '%i.%03i%03i', split /[.]/, $git_version;
211 1         35 return $v;
212             }
213              
214             1;
215              
216             __END__