File Coverage

blib/lib/File/Patch/Undoable.pm
Criterion Covered Total %
statement 61 67 91.0
branch 20 28 71.4
condition 5 8 62.5
subroutine 10 10 100.0
pod 1 1 100.0
total 97 114 85.0


line stmt bran cond sub pod time code
1             package File::Patch::Undoable;
2              
3             our $DATE = '2016-06-09'; # DATE
4             our $VERSION = '0.07'; # VERSION
5              
6 1     1   22983 use 5.010001;
  1         3  
7 1     1   3 use strict;
  1         1  
  1         15  
8 1     1   3 use warnings;
  1         1  
  1         23  
9 1     1   1246 use Log::Any::IfLOG '$log';
  1         10  
  1         4  
10              
11 1     1   430 use Capture::Tiny qw(capture);
  1         4555  
  1         50  
12 1     1   5 use File::Temp qw(tempfile);
  1         0  
  1         34  
13 1     1   402 use IPC::System::Options 'system', -log=>1;
  1         2688  
  1         5  
14 1     1   46 use Proc::ChildError qw(explain_child_error);
  1         4  
  1         437  
15              
16             our %SPEC;
17              
18             sub _check_patch_has_dry_run_option {
19             # some versions of the 'patch' program, like that on freebsd, does not
20             # support the needed --dry-run option. we currently can't run on those
21             # systems.
22              
23             # this currently doesn't work on openbsd, since openbsd's patch does not
24             # exit non-zero if fed unknown options.
25             #my (undef, undef, $exit) = capture { system "patch --dry-run -v" };
26             #return $exit == 0;
27              
28             # cache result
29 151     151   608 state $res = do {
30             # hm, what about windows?
31             #my $man = qx(man patch);
32             #$man =~ /--dry-run/;
33              
34 1         914971 my $help = qx(patch --help);
35 1         45 $help =~ /--dry-run/;
36             };
37              
38 151         632 $res;
39             }
40              
41             $SPEC{patch} = {
42             v => 1.1,
43             summary => 'Patch a file, with undo support',
44             description => <<'_',
45              
46             On do, will patch file with the supplied patch. On undo, will apply the reverse
47             of the patch.
48              
49             Note: Symlink is currently not permitted (except for the patch file). Patching
50             is currently done with the `patch` program.
51              
52             Unfixable state: file does not exist or not a regular file (directory and
53             symlink included), patch file does not exist or not a regular file (but symlink
54             allowed).
55              
56             Fixed state: file exists, patch file exists, and patch has been applied.
57              
58             Fixable state: file exists, patch file exists, and patch has not been applied.
59              
60             _
61             args => {
62             # naming the args 'path' and 'patch' can be rather error prone
63             file => {
64             summary => 'Path to file to be patched',
65             schema => 'str*',
66             req => 1,
67             pos => 0,
68             },
69             patch => {
70             summary => 'Path to patch file',
71             description => <<'_',
72              
73             Patch can be in unified or context format, it will be autodetected.
74              
75             _
76             schema => 'str*',
77             req => 1,
78             pos => 1,
79             },
80             reverse => {
81             summary => 'Whether to apply reverse of patch',
82             schema => [bool => {default=>0}],
83             cmdline_aliases => {R=>{}},
84             },
85             },
86             features => {
87             tx => {v=>2},
88             idempotent => 1,
89             },
90             deps => {
91             prog => 'patch',
92             },
93             };
94             sub patch {
95 150     150 1 105514024 my %args = @_;
96              
97             # TMP, schema
98 150   50     1026 my $tx_action = $args{-tx_action} // '';
99 150         321 my $dry_run = $args{-dry_run};
100 150         277 my $file = $args{file};
101 150 50       456 defined($file) or return [400, "Please specify file"];
102 150         278 my $patch = $args{patch};
103 150 50       415 defined($patch) or return [400, "Please specify patch"];
104 150         448 my $rev = !!$args{reverse};
105              
106 150 50       435 return [412, "The patch program does not support --dry-run option"]
107             unless _check_patch_has_dry_run_option();
108              
109 150         1613 my $is_sym = (-l $file);
110 150         1081 my @st = stat($file);
111 150   66     1527 my $exists = $is_sym || (-e _);
112 150         299 my $is_file = (-f _);
113 150         1039 my $patch_exists = (-e $patch);
114 150         344 my $patch_is_file = (-f _);
115              
116 150         284 my @cmd;
117              
118 150 100       672 if ($tx_action eq 'check_state') {
    50          
119 86 100       296 return [412, "File $file does not exist"] unless $exists;
120 82 100 66     866 return [412, "File $file is not a regular file"] if $is_sym||!$is_file;
121 80 100       269 return [412, "Patch $patch does not exist"] unless $patch_exists;
122 78 100       452 return [412,"Patch $patch is not a regular file"] unless $patch_is_file;
123              
124             # check whether patch has been applied by testing the reverse patch
125 76         463 @cmd = ("patch", "--dry-run", "-sf", "-r","-", ("-R")x!$rev,
126             $file, "-i",$patch);
127 76         482 system @cmd;
128 76 100       311742 if (!$?) {
    50          
129 10         693 return [304, "Patch $patch already applied to $file"];
130             } elsif (($? >> 8) == 1) {
131 66 50       412 $log->info("(DRY) Patching file $file with $patch ...") if $dry_run;
132 66         5679 return [200, "File $file needs to be patched with $patch", undef,
133             {undo_actions=>[
134             [patch=>{file=>$file, patch=>$patch, reverse=>!$rev}],
135             ]}];
136             } else {
137 0         0 return [500, "Can't patch: ".explain_child_error()];
138             }
139              
140             } elsif ($tx_action eq 'fix_state') {
141 64         711 $log->info("Patching file $file with $patch ...");
142              
143             # first patch to a temporary output first, because patch can produce
144             # half-patched file.
145 64         643 my ($tmpfh, $tmpname) = tempfile(DIR=>".");
146              
147 64         24045 @cmd = ("patch", "-sf","-r","-", ("-R")x!!$rev,
148             $file, "-i",$patch, "-o", $tmpname);
149 64         346 system @cmd;
150 64 50       224905 if ($?) {
151 0         0 unlink $tmpname;
152 0         0 return [500, "Can't patch: ".explain_child_error()];
153             }
154              
155             # now rename the temp file to the original file
156 64 50       4659 unless (rename $tmpname, $file) {
157 0         0 unlink $tmpname;
158 0         0 return [500, "Can't rename $tmpname -> $file: $!"];
159             }
160              
161 64         5934 return [200, "OK"];
162             }
163 0           [400, "Invalid -tx_action"];
164             }
165              
166             1;
167             # ABSTRACT: Patch a file, with undo support
168              
169             __END__
170              
171             =pod
172              
173             =encoding UTF-8
174              
175             =head1 NAME
176              
177             File::Patch::Undoable - Patch a file, with undo support
178              
179             =head1 VERSION
180              
181             This document describes version 0.07 of File::Patch::Undoable (from Perl distribution File-Patch-Undoable), released on 2016-06-09.
182              
183             =head1 KNOWN ISSUES
184              
185             =head1 FUNCTIONS
186              
187              
188             =head2 patch(%args) -> [status, msg, result, meta]
189              
190             Patch a file, with undo support.
191              
192             On do, will patch file with the supplied patch. On undo, will apply the reverse
193             of the patch.
194              
195             Note: Symlink is currently not permitted (except for the patch file). Patching
196             is currently done with the C<patch> program.
197              
198             Unfixable state: file does not exist or not a regular file (directory and
199             symlink included), patch file does not exist or not a regular file (but symlink
200             allowed).
201              
202             Fixed state: file exists, patch file exists, and patch has been applied.
203              
204             Fixable state: file exists, patch file exists, and patch has not been applied.
205              
206             This function is not exported.
207              
208             This function is idempotent (repeated invocations with same arguments has the same effect as single invocation). This function supports transactions.
209              
210              
211             Arguments ('*' denotes required arguments):
212              
213             =over 4
214              
215             =item * B<file>* => I<str>
216              
217             Path to file to be patched.
218              
219             =item * B<patch>* => I<str>
220              
221             Path to patch file.
222              
223             Patch can be in unified or context format, it will be autodetected.
224              
225             =item * B<reverse> => I<bool> (default: 0)
226              
227             Whether to apply reverse of patch.
228              
229             =back
230              
231             Special arguments:
232              
233             =over 4
234              
235             =item * B<-tx_action> => I<str>
236              
237             For more information on transaction, see L<Rinci::Transaction>.
238              
239             =item * B<-tx_action_id> => I<str>
240              
241             For more information on transaction, see L<Rinci::Transaction>.
242              
243             =item * B<-tx_recovery> => I<str>
244              
245             For more information on transaction, see L<Rinci::Transaction>.
246              
247             =item * B<-tx_rollback> => I<str>
248              
249             For more information on transaction, see L<Rinci::Transaction>.
250              
251             =item * B<-tx_v> => I<str>
252              
253             For more information on transaction, see L<Rinci::Transaction>.
254              
255             =back
256              
257             Returns an enveloped result (an array).
258              
259             First element (status) is an integer containing HTTP status code
260             (200 means OK, 4xx caller error, 5xx function error). Second element
261             (msg) is a string containing error message, or 'OK' if status is
262             200. Third element (result) is optional, the actual result. Fourth
263             element (meta) is called result metadata and is optional, a hash
264             that contains extra information.
265              
266             Return value: (any)
267              
268             =head1 FAQ
269              
270             =head2 Why use the patch program? Why not use a Perl module like Text::Patch?
271              
272             The B<patch> program has many nice features that L<Text::Patch> lacks, e.g.
273             applying reverse patch (needed to check fixed state and to undo), autodetection
274             of patch type, ignoring whitespace and fuzz factor, etc.
275              
276             =head1 HOMEPAGE
277              
278             Please visit the project's homepage at L<https://metacpan.org/release/File-Patch-Undoable>.
279              
280             =head1 SOURCE
281              
282             Source repository is at L<https://github.com/perlancar/perl-File-Patch-Undoable>.
283              
284             =head1 BUGS
285              
286             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=File-Patch-Undoable>
287              
288             When submitting a bug or request, please include a test-file or a
289             patch to an existing test-file that illustrates the bug or desired
290             feature.
291              
292             =head1 SEE ALSO
293              
294             L<Rinci::Transaction>
295              
296             L<Text::Patch>, L<PatchReader>, L<Text::Patch::Rred>
297              
298             =head1 AUTHOR
299              
300             perlancar <perlancar@cpan.org>
301              
302             =head1 COPYRIGHT AND LICENSE
303              
304             This software is copyright (c) 2016 by perlancar@cpan.org.
305              
306             This is free software; you can redistribute it and/or modify it under
307             the same terms as the Perl 5 programming language system itself.
308              
309             =cut