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-10'; # DATE
4             our $VERSION = '0.08'; # VERSION
5              
6 1     1   27244 use 5.010001;
  1         4  
7 1     1   7 use strict;
  1         1  
  1         28  
8 1     1   5 use warnings;
  1         2  
  1         42  
9 1     1   1648 use Log::Any::IfLOG '$log';
  1         10  
  1         4  
10              
11 1     1   427 use Capture::Tiny qw(capture);
  1         5033  
  1         97  
12 1     1   7 use File::Temp qw(tempfile);
  1         1  
  1         60  
13 1     1   454 use IPC::System::Options 'system', -log=>1;
  1         2793  
  1         6  
14 1     1   47 use Proc::ChildError qw(explain_child_error);
  1         5  
  1         450  
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   468 state $res = do {
30             # hm, what about windows?
31             #my $man = qx(man patch);
32             #$man =~ /--dry-run/;
33              
34 1         2561 my $help = qx(patch --help);
35 1         16 $help =~ /--dry-run/;
36             };
37              
38 151         548 $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 56361886 my %args = @_;
96              
97             # TMP, schema
98 150   50     1023 my $tx_action = $args{-tx_action} // '';
99 150         456 my $dry_run = $args{-dry_run};
100 150         386 my $file = $args{file};
101 150 50       491 defined($file) or return [400, "Please specify file"];
102 150         258 my $patch = $args{patch};
103 150 50       389 defined($patch) or return [400, "Please specify patch"];
104 150         455 my $rev = !!$args{reverse};
105              
106 150 50       474 return [412, "The patch program does not support --dry-run option"]
107             unless _check_patch_has_dry_run_option();
108              
109 150         1600 my $is_sym = (-l $file);
110 150         1073 my @st = stat($file);
111 150   66     1573 my $exists = $is_sym || (-e _);
112 150         403 my $is_file = (-f _);
113 150         1091 my $patch_exists = (-e $patch);
114 150         320 my $patch_is_file = (-f _);
115              
116 150         156 my @cmd;
117              
118 150 100       614 if ($tx_action eq 'check_state') {
    50          
119 86 100       326 return [412, "File $file does not exist"] unless $exists;
120 82 100 66     772 return [412, "File $file is not a regular file"] if $is_sym||!$is_file;
121 80 100       273 return [412, "Patch $patch does not exist"] unless $patch_exists;
122 78 100       384 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         609 @cmd = ("patch", "--dry-run", "-sf", "-r","-", ("-R")x!$rev,
126             $file, "-i",$patch);
127 76         482 system @cmd;
128 76 100       341640 if (!$?) {
    50          
129 10         782 return [304, "Patch $patch already applied to $file"];
130             } elsif (($? >> 8) == 1) {
131 66 50       380 $log->info("(DRY) Patching file $file with $patch ...") if $dry_run;
132 66         5195 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         773 $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         719 my ($tmpfh, $tmpname) = tempfile(DIR=>".");
146              
147 64         25326 @cmd = ("patch", "-sf","-r","-", ("-R")x!!$rev,
148             $file, "-i",$patch, "-o", $tmpname);
149 64         404 system @cmd;
150 64 50       226919 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       5012 unless (rename $tmpname, $file) {
157 0         0 unlink $tmpname;
158 0         0 return [500, "Can't rename $tmpname -> $file: $!"];
159             }
160              
161 64         6494 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.08 of File::Patch::Undoable (from Perl distribution File-Patch-Undoable), released on 2016-06-10.
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