3 # Written by Adam Byrtek <alpha@debian.org>, 2002
4 # Rewritten by David Sterba <dave@jikos.cz>, 2009
6 # Extfs to handle patches in context and unified diff format.
7 # Known issues: When name of file to patch is modified during editing,
8 # hunk is duplicated on copyin. It is unavoidable.
13 use File::Temp 'tempfile';
20 my $fileutil = 'file -b';
22 # date parsing requires Date::Parse from TimeDate module
23 my $parsedates = eval 'require Date::Parse';
26 my $unified_header=qr/^--- .*\t.*\n\+\+\+ .*\t.*\n$/;
27 my $unified_extract=qr/^--- ([^\t]+).*\n\+\+\+ ([^\t]+)\s*(.*)\n/;
28 my $unified_header2=qr/^--- .*\n\+\+\+ .*\n$/;
29 my $unified_extract2=qr/^--- ([^\s]+).*\n\+\+\+ ([^\s]+)\s*(.*)\n/;
30 my $unified_contents=qr/^([+\-\\ \n]|@@ .* @@)/;
31 my $unified_hunk=qr/@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*\n/;
33 my $context_header=qr/^\*\*\* .*\t.*\n--- .*\t.*\n$/;
34 my $context_extract=qr/^\*\*\* ([^\t]+).*\n--- ([^\t]+)\s*(.*)\n/;
35 my $context_header2=qr/^\*\*\* .*\n--- .*\n$/;
36 my $context_extract2=qr/^\*\*\* ([^\s]+).*\n--- ([^\s]+)\s*(.*)\n/;
37 my $context_contents=qr/^([!+\-\\ \n]|-{3} .* -{4}|\*{3} .* \*{4}|\*{15})/;
39 my $ls_extract_id=qr/^[^\s]+\s+[^\s]+\s+([^\s]+)\s+([^\s]+)/;
40 my $basename=qr|^(.*/)*([^/]+)$|;
42 sub patchfs_canonicalize_path ($) {
45 $fname =~ s,(^|/)(?:\.?\./)+,$1,;
49 # output unix date in a mc-readable format
52 my @time=localtime($_[0]);
53 return sprintf '%02d-%02d-%02d %02d:%02d', $time[4]+1, $time[3],
54 $time[5]+1900, $time[2], $time[1];
57 # parse given string as a date and return unix time
60 # in case of problems fall back to 0 in unix time
61 # note: str2time interprets some wrong values (eg. " ") as 'today'
62 if ($parsedates && defined (my $t=str2time($_[0]))) {
68 # print message on stderr and exit
71 print STDERR $_[0], "\n";
78 my ($qfname)=(quotemeta $_[0]);
80 $_=`$fileutil $qfname`;
82 return "$lzma -dc $qfname";
84 return "$xz -dc $qfname";
86 return "$bzip -dc $qfname";
88 return "$gzip -dc $qfname";
97 my ($qfname,$append)=(quotemeta $_[0],$_[1]);
98 my ($sep) = $append ? '>>' : '>';
100 $_=`$fileutil $qfname`;
102 return "$lzma -c $sep $qfname";
104 return "$xz -c $sep $qfname";
105 } elsif (/^'*bzip/) {
106 return "$bzip -c $sep $qfname";
107 } elsif (/^'*gzip/) {
108 return "$gzip -c $sep $qfname";
110 return "cat $sep $qfname";
114 # select diff filename conforming with rules found in diff.info
117 my ($fsrc,$fdst)= @_;
118 # TODO: can remove these two calls later
119 $fsrc = patchfs_canonicalize_path ($fsrc);
120 $fdst = patchfs_canonicalize_path ($fdst);
121 if (!$fdst && !$fsrc) {
122 error 'Index: not yet implemented';
123 } elsif (!$fsrc || $fsrc eq '/dev/null') {
124 return ($fdst,'PATCH-CREATE/');
125 } elsif (!$fdst || $fdst eq '/dev/null') {
126 return ($fsrc,'PATCH-REMOVE/');
127 } elsif (($fdst eq '/dev/null') && ($fsrc eq '/dev/null')) {
128 error 'Malformed diff, missing a sane filename';
130 # fewest path name components
131 if ($fdst=~s|/|/|g < $fsrc=~s|/|/|g) {
133 } elsif ($fdst=~s|/|/|g > $fsrc=~s|/|/|g) {
137 if (($fdst=~/$basename/o,length $2) < ($fsrc=~/$basename/o,length $2)) {
139 } elsif (($fdst=~/$basename/o,length $2) > ($fsrc=~/$basename/o,length $2)) {
143 if (length $fdst < length $fsrc) {
153 # IN: diff "archive" name
154 # IN: file handle for output; STDIN for list, tempfile else
155 # IN: filename to watch (for: copyout, rm), '' for: list
156 # IN: remove the file?
157 # true - ... and print out the rest
158 # false - ie. copyout mode, print just the file
161 my $archive=quotemeta shift;
165 my ($state,$fsize,$time);
166 my ($f,$fsrc,$fdst,$prefix);
167 my ($unified,$context);
168 my ($skipread, $filetoprint, $filefound);
169 my ($h_add,$h_del,$h_ctx); # hunk line counts
170 my ($h_r1,$h_r2); # hunk ranges
171 my @outsrc; # if desired ...
177 import Date::Parse if ($parsedates && $file eq '');
180 $state=0; $fsize=0; $f='';
182 while ($skipread || ($line++,$_=<I>)) {
184 if($state == 0) { # expecting comments
186 $unified=1 if (/^--- /);
187 $context=1 if (/^\*\*\* /);
188 if (!$unified && !$context) {
189 $filefound=0 if($file ne '' && $filetoprint);
190 # shortcut for rmmod xor filefound
191 # - in rmmod we print if not found
192 # - in copyout (!rmmod) we print if found
193 print $fh $_ if($rmmod != $filefound);
197 if($file eq '' && $filetoprint) {
198 $fmap_size{"$prefix$f"}+=$fsize;
199 $fmap_time{"$prefix$f"}=$time;
203 $_ .=<I>; # steal next line, both formats
206 if(/$unified_header/o) {
207 ($fsrc,$fdst,$time) = /$unified_extract/o;
208 } elsif(/$unified_header2/o) {
209 ($fsrc,$fdst,$time) = /$unified_extract2/o;
211 error "Can't parse unified diff header";
214 if(/$context_header/o) {
215 ($fsrc,$fdst,$time) = /$context_extract/o;
216 } elsif(/$context_header2/o) {
217 ($fsrc,$fdst,$time) = /$context_extract2/o;
219 error "Can't parse context diff header";
222 error "Unrecognized diff header";
224 $fsrc=patchfs_canonicalize_path($fsrc);
225 $fdst=patchfs_canonicalize_path($fdst);
230 ($f,$prefix)=diff_filename($fsrc,$fdst);
231 $filefound=($f eq $file);
236 print $fh $_ if($rmmod != $filefound);
239 } elsif($state == 1) { # expecting diff hunk headers, end of file or comments
242 ($a,$b,$h_r1,$c,$d,$h_r2)=/$unified_hunk/o;
243 if(!defined($a) || !defined($c)) {
244 # hunk header does not come, a comment inside
245 # or maybe a new file, state 0 will decide
251 print $fh $_ if($rmmod != $filefound);
252 $h_r1=1 if(!defined($b));
253 $h_r2=1 if(!defined($d));
254 $h_add=$h_del=$h_ctx=0;
257 if(!/$context_contents/o) {
262 print $fh $_ if($rmmod != $filefound);
265 } elsif($state == 2) { # expecting hunk contents
266 if($h_del + $h_ctx == $h_r1 && $h_add + $h_ctx == $h_r2) {
267 # hooray, end of hunk
268 # we optimistically ended with a hunk before but
269 # the line has been read already
274 print $fh $_ if($rmmod != $filefound);
277 if(ord($first) == ord('+')) { $h_add++; }
278 elsif(ord($first) == ord('-')) { $h_del++; }
279 elsif(ord($first) == ord(' ')) { $h_ctx++; }
280 elsif(ord($first) == ord('\\')) { 0; }
281 elsif(ord($first) == ord('@')) { error "Malformed hunk, header came too early"; }
282 else { error "Unrecognized character in hunk"; }
285 if($file eq '' && $filetoprint) {
286 $fmap_size{"$prefix$f"}+=$fsize;
287 $fmap_time{"$prefix$f"}=$time;
290 # use uid and gid from file
291 my ($uid,$gid)=(`ls -l $archive`=~/$ls_extract_id/o);
293 # flush all file names with cumulative file size
294 while(my ($fn, $fs) = each %fmap_size) {
295 printf $fh "-rw-r--r-- 1 %s %s %d %s %s\n", $uid, $gid, $fs, datetime($fmap_time{$fn}), $fn;
298 close($fh) if($file ne '');
299 return \(@outsrc, @outdst) if wantarray;
302 # list files affected by patch
304 parse($_[0], *STDOUT, '', 0);
308 # extract diff from patch
309 # IN: diff file to find
310 # IN: output file name
314 $file=~s/^(PATCH-(CREATE|REMOVE)\/)?(.*)\.diff$/$3/;
315 $file = patchfs_canonicalize_path ($file);
317 open(FH, ">$out") or error("Cannot open output file");
318 parse('', *FH, $file, 0);
321 # remove diff(s) from patch
326 my ($tmp,$tmpname)=tempfile();
328 @_=map {scalar(s/^(PATCH-(CREATE|REMOVE)\/)?(.*)\.diff$/$3/,$_)} @_;
330 # just the first file for now
331 parse($archive, $tmp, $_[0], 1);
335 system("cat \Q$tmpname\E | " . myout($archive,0))==0
336 or error "Can't write to archive";
337 system("rm -f -- \Q$tmpname\E");
340 # append diff to archive
341 # IN: diff archive name
342 # IN: newly created file name in archive
343 # IN: the real source file
345 # TODO: seems to be tricky. what to do?
346 # copyin of file which is already there may:
347 # * delete the original and copy only the new
348 # * just append the new hunks to the same file
349 # problems: may not be a valid diff, unmerged hunks
350 # * try to merge the two together
351 # ... but we do not want write patchutils again, right?
352 error "Copying files into diff not supported";
355 my ($archive,$name,$src)=@_;
357 # in case we are appending another diff, we have
358 # to delete/merge all the files
359 open(DEVNULL, ">/dev/null");
360 open I, myin($src).'|';
361 my ($srclist,$dstlist)=parse($archive, *DEVNULL, '', 0);
365 print("SRC: del $_\n");
368 print("DST: del $_\n");
372 # remove overwritten file
373 open I, myin($archive).'|';
374 rm ($archive, $name);
377 my $cmd1=myin("$src.diff");
378 my $cmd2=myout($archive,1);
379 system("$cmd1 | $cmd2")==0
380 or error "Can't write to archive";
387 $fin = readlink $fin;
390 if ($ARGV[0] eq 'list') {
391 open I, myin($fin).'|';
394 } elsif ($ARGV[0] eq 'copyout') {
395 open I, myin($fin)."|";
396 copyout ($ARGV[2], $ARGV[3]);
398 } elsif ($ARGV[0] eq 'rm') {
399 open I, myin($fin)."|";
402 } elsif ($ARGV[0] eq 'rmdir') {
404 } elsif ($ARGV[0] eq 'mkdir') {
406 } elsif ($ARGV[0] eq 'copyin') {
407 copyin ($fin, $ARGV[2], $ARGV[3]);