File Coverage

blib/lib/SVN/Deploy/Utils.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package SVN::Deploy::Utils;
2            
3 1     1   19 use strict;
  1         2  
  1         40  
4 1     1   5 use warnings;
  1         1  
  1         59  
5            
6             our $VERSION = '0.11';
7            
8 1     1   4 use Carp;
  1         2  
  1         100  
9 1     1   543 use SVN::Client;
  0            
  0            
10            
11             use Cwd;
12             use Digest::MD5;
13             use File::Spec::Functions qw/:ALL/;
14             use File::Copy;
15             use File::Temp qw/tempdir/;
16            
17             use Data::Dumper;
18             $Data::Dumper::Indent=1;
19            
20            
21             our $Cleanup = 1;
22             our $Verbose = 0;
23             our $LastErr = '';
24            
25            
26             my %arg_check = (
27             connect_cached => {qw/username o password o pwd_sub o/},
28             import_synch => {qw/
29             dir m url m log o
30             checkout o ctx o
31             /},
32             );
33            
34            
35             sub _vlog($@) { print join(' ', @_), "\n" if $Verbose; };
36            
37            
38             sub _getargs {
39             my $caller = (caller(1))[3];
40             croak "odd number of arguments for $caller()"
41             unless @_ % 2 == 0;
42            
43             $caller =~ s/.*::(\w+)$/$1/;
44             my %tmp = @_;
45            
46             for my $arg ( keys( %{ $arg_check{$caller} } ) ) {
47             next if $arg_check{$caller}{$arg} ne 'm';
48             croak "$caller: mandatory parameter '$arg' missing or empty"
49             unless $tmp{$arg};
50             }
51            
52             for my $arg ( keys( %tmp ) ) {
53             croak "$caller: unknown parameter '$arg'"
54             unless exists($arg_check{$caller}{$arg});
55             }
56            
57             return(@_);
58             }
59            
60            
61             sub _getmd5 {
62             my($fn) = @_;
63            
64             open(my $fh, '<', $fn)
65             or croak "couldn't read '$fn', $!";
66             binmode($fh);
67             my $md5 = Digest::MD5->new()->addfile($fh)->hexdigest();
68             close($fh);
69            
70             return($md5);
71             }
72            
73            
74             sub _svn {
75             my $ctx = shift;
76             my $call = shift;
77            
78             my @ret = $ctx->$call(@_);
79            
80             if ( ref($ret[0]) eq '_p_svn_error_t' ) {
81             $LastErr = "svn call $call(" . join(', ', @_) . ') failed, '
82             . $ret[0]->expanded_message();
83             return;
84             }
85            
86             return(wantarray ? @ret : ($ret[0] || 1) );
87             };
88            
89            
90             sub _simple_prompt {
91             my($realm) = @_;
92             my %cred;
93             print "Logon information for $realm\n";
94             for my $par ( qw/username password/ ) {
95             print ucfirst($par), ": ";
96             $cred{$par} = ;
97             chomp($cred{$par});
98             }
99             return(@cred{qw/username password/});
100             }
101            
102            
103             sub connect_cached {
104             my %args = _getargs(@_);
105            
106             my $ctx = SVN::Client->new(
107             auth => [
108             SVN::Client::get_simple_provider(),
109             SVN::Client::get_simple_prompt_provider(sub {
110             unless ( $args{username} and $args{password} ) {
111             my $subref = ref($args{pwd_sub}) eq 'CODE'
112             ? $args{pwd_sub}
113             : \&_simple_prompt;
114             @args{qw/username password/} = $subref->($_[1]);
115             }
116             $_[0]->username($args{username});
117             $_[0]->password($args{password});
118             $_[0]->may_save(1);
119             }, 2),
120             SVN::Client::get_username_provider()
121             ],
122             );
123            
124             return($ctx);
125             }
126            
127            
128             sub import_synch {
129             my %args = _getargs(@_);
130            
131             my $ctx = $args{ctx} || connect_cached();
132            
133             $args{dir} = rel2abs($args{dir});
134            
135             my $tempdir = tempdir(
136             'SVN-Deploy-Utils-XXXXXX',
137             CLEANUP => $Cleanup,
138             TMPDIR => 1,
139             );
140            
141             my $origdir = getcwd();
142            
143             if ( $args{log} ) {
144             _svn($ctx, 'log_msg', sub { ${$_[0]} = $args{log}; })
145             or return;
146             }
147            
148             # iterating over svn dir
149             # - locally missing -> delete in svn
150             # - locally name matches, type differs -> delete in svn
151             _vlog "pass 1: check for deleted items";
152             my @dstack;
153             my %todo;
154             my %ent_cache;
155             my $last_commit_revnum = -1;
156             do {{
157             my $suburl = join('/', @dstack);
158             $suburl = ' ' unless length($suburl);
159             my $url = join('/', $args{url}, @dstack);
160             my $subdir = catdir(@dstack);
161             my $dir = catdir($args{dir}, $subdir);
162            
163             # get entries for $url unless already done
164             unless ( $todo{$suburl} ) {
165            
166             _vlog "getting entries for $url";
167            
168             my $entries_href = _svn($ctx, 'ls', $url, 'HEAD', 0)
169             or return;
170            
171             _vlog Dumper($entries_href);
172            
173             $todo{$suburl} = [
174             map { {
175             name => $_,
176             kind => $entries_href->{$_}->kind,
177             time => $entries_href->{$_}->time,
178             size => $entries_href->{$_}->size,
179            
180             } } keys(%$entries_href)
181             ];
182            
183             # cache entries for later
184             $ent_cache{ join('/', $url, $_) }
185             = $entries_href->{$_} for keys(%$entries_href);
186             }
187            
188             my $node = shift(@{$todo{$suburl}});
189            
190             # all nodes processed -> one up
191             unless ( defined($node) ) {
192             _vlog " --> no more nodes in $suburl, going back";
193             pop(@dstack);
194             delete($todo{$suburl}) unless $suburl eq ' ';
195             next;
196             }
197            
198             my $locfile = catfile($dir, $node->{name});
199             my $svnfile = join('/', $url, $node->{name});
200             my $svnshort = join('/', @dstack, $node->{name});
201            
202             _vlog " --> processing node '$svnshort'";
203            
204             # process node
205             if ( $node->{kind} == $SVN::Node::dir ) {
206             if ( -d $locfile ) {
207             _vlog " --> dir: pushing on stack";
208             push(@dstack, $node->{name});
209             } else {
210             _vlog " --> locally deleted or type changed -> deleting";
211             my $info = _svn($ctx, 'delete', $svnfile, 1)
212             or return;
213             $last_commit_revnum = $info->revision;
214             delete($ent_cache{$svnfile});
215             }
216             } else {
217             next if -e $locfile and !-d $locfile;
218             _vlog " --> locally deleted or type changed -> deleting";
219             my $info = _svn($ctx, 'delete', $svnfile, 1)
220             or return;
221             $last_commit_revnum = $info->revision;
222             delete($ent_cache{$svnfile});
223             }
224            
225             }} while @dstack or @{$todo{' '}};
226            
227            
228             # iterating over external dir
229             # - new dirs -> mkdir in repo
230             # - new files -> add to repo, add MD5 property
231             # - external file time > repo file time
232             # or external file size != repo file size
233             # or external file MD5 != MD5 property
234             # -> commit, set md5 property
235             _vlog "pass 2: check for new or changed items";
236             @dstack = ();
237             %todo = ();
238             my %to_commit;
239             do {{
240             my $suburl = join('/', @dstack);
241             my $url = join('/', $args{url}, @dstack);
242             my $subdir = catdir(@dstack);
243             $subdir = ' ' unless length($subdir);
244             my $dir = catdir($args{dir}, @dstack);
245            
246             # get entries for $dir unless already done
247             unless ( $todo{$subdir} ) {
248             opendir(my $dh, $dir)
249             or croak "couldn't open dir '$dir', $!";
250             $todo{$subdir} = [grep {not /^\.{1,2}$/} readdir($dh)];
251             }
252            
253             my $node = shift(@{$todo{$subdir}});
254            
255             # all nodes processed -> one up
256             unless ( defined($node) ) {
257             _vlog " --> no more nodes in $subdir, going back";
258             pop(@dstack);
259             delete($todo{$subdir}) unless $subdir eq ' ';
260             next;
261             }
262            
263             my $locfile = catfile($dir, $node);
264             my $svnfile = join('/', $url, $node);
265             my $locshort = catfile(@dstack, $node);
266            
267             _vlog " --> processing node '$locshort'";
268            
269             # process node
270             my $svnent = $ent_cache{$svnfile};
271            
272             if ( -d $locfile ) {
273            
274             unless ( defined($svnent) ) {
275             _vlog " --> dir: creating in svn";
276             _svn($ctx, 'mkdir', $svnfile) or return;
277             }
278             _vlog " --> dir: pushing on stack";
279             push(@dstack, $node);
280            
281             } else {
282            
283             my($svn_md5, $loc_md5);
284             my $state = 'new';
285            
286             if ( defined($svnent) ) {
287            
288             # exists in svn -> compare
289             my $svn_time = $svnent->time;
290             my $svn_size = $svnent->size;
291             $svn_md5
292             = _svn($ctx, 'propget', 'md5', $svnfile, 'HEAD', 0);
293             $svn_md5 = ($svn_md5 && $svn_md5->{$svnfile})
294             ? $svn_md5->{$svnfile}
295             : '';
296             substr($svn_time, -6) = '';
297            
298             my $loc_time = (stat($locfile))[9];
299             my $loc_size = -s $locfile;
300             $loc_md5 = _getmd5($locfile);
301            
302             next if $loc_size == 0 and $svn_size == 0;
303            
304             my $changed
305             = (
306             $loc_size != $svn_size
307             or $loc_time > $svn_time
308             or $loc_md5 ne $svn_md5
309             );
310            
311             if ( $changed ) {
312             $state = 'changed';
313             } else {
314             next;
315             }
316             } else {
317             $loc_md5 = _getmd5($locfile);
318             }
319            
320             _vlog " --> $state file: adding to svn";
321            
322             # copying file to workdir
323             unless ( $to_commit{$subdir} and -d $to_commit{$subdir} ) {
324             my $tempsub = catdir($tempdir, join('-', @dstack) || 'root' );
325             _vlog " --> checkout '$url' to '$tempsub'";
326             _svn($ctx, 'checkout', $url, $tempsub, 'HEAD', 0)
327             or return;
328             $to_commit{$subdir} = $tempsub;
329             }
330             my $workfile = catfile($to_commit{$subdir}, $node);
331             copy($locfile, $workfile)
332             or croak "couldn't copy '$locfile' to '$workfile', $!";
333            
334             chdir($to_commit{$subdir});
335             if ( $state eq 'new' ) {
336             _svn($ctx, 'add', $node, 0)
337             or return;
338             }
339             _svn($ctx, 'propset', 'md5', $loc_md5, $node, 0)
340             or return;
341             }
342            
343             }} while @dstack or @{$todo{' '}};
344            
345             for my $v ( values(%to_commit) ) {
346             my $info = _svn($ctx, 'commit', $v, 0)
347             or return;
348             $last_commit_revnum = $info->revision
349             if $info->revision != $SVN::Core::INVALID_REVNUM;
350             }
351            
352             chdir($origdir);
353            
354             return($last_commit_revnum);
355             }
356            
357            
358             1;
359            
360             ## POD
361             =head1 NAME
362            
363             SVN::Deploy::Utils - utility functions for SVN::Deploy
364            
365             =head1 SYNOPSIS
366            
367             use SVN::Deploy::Utils;
368            
369             my $rev = import_synch(
370             dir => '/my/local/dir',
371             url => 'svn://myrepo/trunk/mypath',
372             log => 'my import logmessage',
373             ) or die $SVN::Deploy::Utils::LastErr;
374            
375             =head1 DESCRIPTION
376            
377             SVN::Deploy::Utils provides two high level utility functions
378             encapsuling SVN::Client methods.
379            
380             =head1 FUNCTIONS
381            
382             All functions return undef on error. $SVN::Deploy::Utils::LastErr will
383             contain a printable error message.
384            
385             =head2 connect_cached
386            
387             my $ctx = connect_cached(
388             [username => ,]
389             [password => ,]
390             [pwd_sub => ,]
391             );
392            
393             Returns an SVN::Client context object caching the authorization
394             information for later use. pwd_sub must reference a sub returning
395             username and password for e.g. user interaction.
396            
397             =head2 import_synch
398            
399             my $rev = import_synch(
400             dir => ,
401             url => ,
402             [log => ,]
403             [ctx => ,]
404             )
405            
406             Imports a local directory into a subversion repository. Adds or
407             deletes files and directories when neccessary, so that repeating calls
408             after changes in the local unversioned directory will result in
409             corresponding changes in the repository path.
410            
411             If ctx is specified import_synch will use this context, otherwise it
412             will call connect_cached() without parameters.
413            
414             =head1 AUTHOR
415            
416             Thomas Kratz Etomk@cpan.orgE
417            
418             Copyright (c) 2008 Thomas Kratz. All rights reserved.
419            
420             This library is free software; you can redistribute it and/or modify
421             it under the same terms as Perl itself, either Perl version 5.8.8 or,
422             at your option, any later version of Perl 5 you may have available.
423            
424             =cut