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 = '2017-07-10'; # DATE
4             our $VERSION = '0.09'; # VERSION
5              
6 1     1   64704 use 5.010001;
  1         6  
7 1     1   8 use strict;
  1         4  
  1         30  
8 1     1   8 use warnings;
  1         3  
  1         80  
9 1     1   5131 use Log::ger;
  1         125  
  1         7  
10              
11 1     1   2476 use Capture::Tiny qw(capture);
  1         8550  
  1         93  
12 1     1   11 use File::Temp qw(tempfile);
  1         4  
  1         62  
13 1     1   660 use IPC::System::Options 'system', -log=>1;
  1         5724  
  1         11  
14 1     1   101 use Proc::ChildError qw(explain_child_error);
  1         3  
  1         1070  
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   731 state $res = do {
30             # hm, what about windows?
31             #my $man = qx(man patch);
32             #$man =~ /--dry-run/;
33              
34 1         4703 my $help = qx(patch --help);
35 1         35 $help =~ /--dry-run/;
36             };
37              
38 151         1716 $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 <prog: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 6115834 my %args = @_;
96              
97             # TMP, schema
98 150   50     3693 my $tx_action = $args{-tx_action} // '';
99 150         885 my $dry_run = $args{-dry_run};
100 150         551 my $file = $args{file};
101 150 50       638 defined($file) or return [400, "Please specify file"];
102 150         614 my $patch = $args{patch};
103 150 50       3454 defined($patch) or return [400, "Please specify patch"];
104 150         1086 my $rev = !!$args{reverse};
105              
106 150 50       889 return [412, "The patch program does not support --dry-run option"]
107             unless _check_patch_has_dry_run_option();
108              
109 150         2754 my $is_sym = (-l $file);
110 150         2025 my @st = stat($file);
111 150   66     2076 my $exists = $is_sym || (-e _);
112 150         581 my $is_file = (-f _);
113 150         1836 my $patch_exists = (-e $patch);
114 150         594 my $patch_is_file = (-f _);
115              
116 150         439 my @cmd;
117              
118 150 100       1161 if ($tx_action eq 'check_state') {
    50          
119 86 100       457 return [412, "File $file does not exist"] unless $exists;
120 82 100 66     1465 return [412, "File $file is not a regular file"] if $is_sym||!$is_file;
121 80 100       526 return [412, "Patch $patch does not exist"] unless $patch_exists;
122 78 100       572 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         786 @cmd = ("patch", "--dry-run", "-sf", "-r","-", ("-R")x!$rev,
126             $file, "-i",$patch);
127 76         713 system @cmd;
128 76 100       828226 if (!$?) {
    50          
129 10         2319 return [304, "Patch $patch already applied to $file"];
130             } elsif (($? >> 8) == 1) {
131 66 50       660 log_info("(DRY) Patching file $file with $patch ...") if $dry_run;
132 66         9948 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         1475 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         1648 my ($tmpfh, $tmpname) = tempfile(DIR=>".");
146              
147 64         54202 @cmd = ("patch", "-sf","-r","-", ("-R")x!!$rev,
148             $file, "-i",$patch, "-o", $tmpname);
149 64         649 system @cmd;
150 64 50       723386 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       19583 unless (rename $tmpname, $file) {
157 0         0 unlink $tmpname;
158 0         0 return [500, "Can't rename $tmpname -> $file: $!"];
159             }
160              
161 64         10810 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.09 of File::Patch::Undoable (from Perl distribution File-Patch-Undoable), released on 2017-07-10.
182              
183             =head1 KNOWN ISSUES
184              
185             =head1 FUNCTIONS
186              
187              
188             =head2 patch
189              
190             Usage:
191              
192             patch(%args) -> [status, msg, result, meta]
193              
194             Patch a file, with undo support.
195              
196             On do, will patch file with the supplied patch. On undo, will apply the reverse
197             of the patch.
198              
199             Note: Symlink is currently not permitted (except for the patch file). Patching
200             is currently done with the L<patch> program.
201              
202             Unfixable state: file does not exist or not a regular file (directory and
203             symlink included), patch file does not exist or not a regular file (but symlink
204             allowed).
205              
206             Fixed state: file exists, patch file exists, and patch has been applied.
207              
208             Fixable state: file exists, patch file exists, and patch has not been applied.
209              
210             This function is not exported.
211              
212             This function is idempotent (repeated invocations with same arguments has the same effect as single invocation). This function supports transactions.
213              
214              
215             Arguments ('*' denotes required arguments):
216              
217             =over 4
218              
219             =item * B<file>* => I<str>
220              
221             Path to file to be patched.
222              
223             =item * B<patch>* => I<str>
224              
225             Path to patch file.
226              
227             Patch can be in unified or context format, it will be autodetected.
228              
229             =item * B<reverse> => I<bool> (default: 0)
230              
231             Whether to apply reverse of patch.
232              
233             =back
234              
235             Special arguments:
236              
237             =over 4
238              
239             =item * B<-tx_action> => I<str>
240              
241             For more information on transaction, see L<Rinci::Transaction>.
242              
243             =item * B<-tx_action_id> => I<str>
244              
245             For more information on transaction, see L<Rinci::Transaction>.
246              
247             =item * B<-tx_recovery> => I<str>
248              
249             For more information on transaction, see L<Rinci::Transaction>.
250              
251             =item * B<-tx_rollback> => I<str>
252              
253             For more information on transaction, see L<Rinci::Transaction>.
254              
255             =item * B<-tx_v> => I<str>
256              
257             For more information on transaction, see L<Rinci::Transaction>.
258              
259             =back
260              
261             Returns an enveloped result (an array).
262              
263             First element (status) is an integer containing HTTP status code
264             (200 means OK, 4xx caller error, 5xx function error). Second element
265             (msg) is a string containing error message, or 'OK' if status is
266             200. Third element (result) is optional, the actual result. Fourth
267             element (meta) is called result metadata and is optional, a hash
268             that contains extra information.
269              
270             Return value: (any)
271              
272             =head1 FAQ
273              
274             =head2 Why use the patch program? Why not use a Perl module like Text::Patch?
275              
276             The B<patch> program has many nice features that L<Text::Patch> lacks, e.g.
277             applying reverse patch (needed to check fixed state and to undo), autodetection
278             of patch type, ignoring whitespace and fuzz factor, etc.
279              
280             =head1 HOMEPAGE
281              
282             Please visit the project's homepage at L<https://metacpan.org/release/File-Patch-Undoable>.
283              
284             =head1 SOURCE
285              
286             Source repository is at L<https://github.com/perlancar/perl-File-Patch-Undoable>.
287              
288             =head1 BUGS
289              
290             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=File-Patch-Undoable>
291              
292             When submitting a bug or request, please include a test-file or a
293             patch to an existing test-file that illustrates the bug or desired
294             feature.
295              
296             =head1 SEE ALSO
297              
298             L<Rinci::Transaction>
299              
300             L<Text::Patch>, L<PatchReader>, L<Text::Patch::Rred>
301              
302             =head1 AUTHOR
303              
304             perlancar <perlancar@cpan.org>
305              
306             =head1 COPYRIGHT AND LICENSE
307              
308             This software is copyright (c) 2017, 2016, 2015, 2014, 2012 by perlancar@cpan.org.
309              
310             This is free software; you can redistribute it and/or modify it under
311             the same terms as the Perl 5 programming language system itself.
312              
313             =cut