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/^--- .*\n\+\+\+ .*\n$/;
27 my $unified_extract=qr/^--- ([^\s]+).*\n\+\+\+ ([^\s]+)\s*([^\t\n]*)/;
28 my $unified_contents=qr/^([+\-\\ \n]|@@ .* @@)/;
29 my $unified_hunk=qr/@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+)) @@.*\n/;
31 my $context_header=qr/^\*\*\* .*\n--- .*\n$/;
32 my $context_extract=qr/^\*\*\* ([^\s]+).*\n--- ([^\s]+)\s*([^\t\n]*)/;
33 my $context_contents=qr/^([!+\-\\ \n]|-{3} .* -{4}|\*{3} .* \*{4}|\*{15})/;
35 my $ls_extract_id=qr/^[^\s]+\s+[^\s]+\s+([^\s]+)\s+([^\s]+)/;
36 my $basename=qr|^(.*/)*([^/]+)$|;
38 sub patchfs_canonicalize_path ($) {
41 $fname =~ s,(^|/)(?:\.?\./)+,$1,;
45 # output unix date in a mc-readable format
48 my @time=localtime($_[0]);
49 return sprintf '%02d-%02d-%02d %02d:%02d', $time[4]+1, $time[3],
50 $time[5]+1900, $time[2], $time[1];
53 # parse given string as a date and return unix time
56 # in case of problems fall back to 0 in unix time
57 # note: str2time interprets some wrong values (eg. " ") as 'today'
58 if ($parsedates && defined (my $t=str2time($_[0]))) {
64 # print message on stderr and exit
67 print STDERR $_[0], "\n";
74 my ($qfname)=(quotemeta $_[0]);
76 $_=`$fileutil $qfname`;
78 return "$lzma -dc $qfname";
80 return "$xz -dc $qfname";
82 return "$bzip -dc $qfname";
84 return "$gzip -dc $qfname";
93 my ($qfname,$append)=(quotemeta $_[0],$_[1]);
94 my ($sep) = $append ? '>>' : '>';
96 $_=`$fileutil $qfname`;
98 return "$lzma -c $sep $qfname";
100 return "$xz -c $sep $qfname";
101 } elsif (/^'*bzip/) {
102 return "$bzip -c $sep $qfname";
103 } elsif (/^'*gzip/) {
104 return "$gzip -c $sep $qfname";
106 return "cat $sep $qfname";
110 # select diff filename conforming with rules found in diff.info
113 my ($fsrc,$fdst)= @_;
114 # TODO: can remove these two calls later
115 $fsrc = patchfs_canonicalize_path ($fsrc);
116 $fdst = patchfs_canonicalize_path ($fdst);
117 if (!$fdst && !$fsrc) {
118 error 'Index: not yet implemented';
119 } elsif (!$fsrc || $fsrc eq '/dev/null') {
120 return ($fdst,'PATCH-CREATE/');
121 } elsif (!$fdst || $fdst eq '/dev/null') {
122 return ($fsrc,'PATCH-REMOVE/');
123 } elsif (($fdst eq '/dev/null') && ($fsrc eq '/dev/null')) {
124 error 'Malformed diff';
126 # fewest path name components
127 if ($fdst=~s|/|/|g < $fsrc=~s|/|/|g) {
129 } elsif ($fdst=~s|/|/|g > $fsrc=~s|/|/|g) {
133 if (($fdst=~/$basename/o,length $2) < ($fsrc=~/$basename/o,length $2)) {
135 } elsif (($fdst=~/$basename/o,length $2) > ($fsrc=~/$basename/o,length $2)) {
139 if (length $fdst < length $fsrc) {
149 # IN: diff "archive" name
150 # IN: file handle for output; STDIN for list, tempfile else
151 # IN: filename to watch (for: copyout, rm), '' for: list
152 # IN: remove the file?
153 # true - ... and print out the rest
154 # false - ie. copyout mode, print just the file
157 my $archive=quotemeta shift;
161 my ($state,$fsize,$time);
162 my ($f,$fsrc,$fdst,$prefix);
163 my ($unified,$context);
164 my ($skipread, $filetoprint, $filefound);
165 my ($h_add,$h_del,$h_ctx); # hunk line counts
166 my ($h_r1,$h_r2); # hunk ranges
167 my @outsrc; # if desired ...
171 # use uid and gid from file
172 my ($uid,$gid)=(`ls -l $archive`=~/$ls_extract_id/o);
174 import Date::Parse if ($parsedates && $file eq '');
177 $state=0; $fsize=0; $f='';
179 while ($skipread || ($line++,$_=<I>)) {
181 if($state == 0) { # expecting comments
183 $unified=1 if (/^--- /);
184 $context=1 if (/^\*\*\* /);
185 if (!$unified && !$context) {
186 $filefound=0 if($file ne '' && $filetoprint);
187 # shortcut for rmmod xor filefound
188 # - in rmmod we print if not found
189 # - in copyout (!rmmod) we print if found
190 print $fh $_ if($rmmod != $filefound);
194 if($file eq '' && $filetoprint) {
195 printf $fh "-rw-r--r-- 1 %s %s %d %s %s%s\n", $uid, $gid, $fsize, datetime($time), $prefix, $f;
199 $_ .=<I>; # steel next line, both formats
202 if(/$unified_header/o) {
203 ($fsrc,$fdst,$time) = /$unified_extract/o;
205 error "Can't parse unified diff header";
208 if(/$context_header/o) {
209 ($fsrc,$fdst,$time) = /$context_extract/o;
211 error "Can't parse context diff header";
214 error "Unrecognized diff header";
216 $fsrc=patchfs_canonicalize_path($fsrc);
217 $fdst=patchfs_canonicalize_path($fdst);
222 ($f,$prefix)=diff_filename($fsrc,$fdst);
223 $filefound=($fsrc eq $file || $fdst eq $file);
228 print $fh $_ if($rmmod != $filefound);
231 } elsif($state == 1) { # expecting diff hunk headers, end of file or comments
234 ($a,$b,$h_r1,$c,$d,$h_r2)=/$unified_hunk/o;
235 if(!defined($a) || !defined($c)) {
236 # hunk header does not come, a comment inside
237 # or maybe a new file, state 0 will decide
243 print $fh $_ if($rmmod != $filefound);
244 $h_r1=1 if(!defined($b));
245 $h_r2=1 if(!defined($d));
246 $h_add=$h_del=$h_ctx=0;
249 if(!/$context_contents/o) {
254 print $fh $_ if($rmmod != $filefound);
257 } elsif($state == 2) { # expecting hunk contents
258 if($h_del + $h_ctx == $h_r1 && $h_add + $h_ctx == $h_r2) {
259 # hooray, end of hunk
260 # we optimistically ended with a hunk before but
261 # the line has been read already
266 print $fh $_ if($rmmod != $filefound);
269 if(ord($first) == ord('+')) { $h_add++; }
270 elsif(ord($first) == ord('-')) { $h_del++; }
271 elsif(ord($first) == ord(' ')) { $h_ctx++; }
272 elsif(ord($first) == ord('\\')) { 0; }
273 elsif(ord($first) == ord('@')) { error "Malformed hunk, header came too early"; }
274 else { error "Unrecognized character in hunk"; }
277 if($file eq '' && $filetoprint) {
278 printf $fh "-rw-r--r-- 1 %s %s %d %s %s%s\n", $uid, $gid, $fsize, datetime($time), $prefix, $f;
281 close($fh) if($file ne '');
282 return \(@outsrc, @outdst) if wantarray;
285 # list files affected by patch
287 parse($_[0], *STDOUT, '', 0);
291 # extract diff from patch
292 # IN: diff file to find
293 # IN: output file name
297 $file=~s/^(PATCH-(CREATE|REMOVE)\/)?(.*)\.diff$/$3/;
298 $file = patchfs_canonicalize_path ($file);
300 open(FH, ">$out") or error("Cannot open output file");
301 parse('', *FH, $file, 0);
304 # remove diff(s) from patch
309 my ($tmp,$tmpname)=tempfile();
311 @_=map {scalar(s/^(PATCH-(CREATE|REMOVE)\/)?(.*)\.diff$/$3/,$_)} @_;
313 # just the first file for now
314 parse($archive, $tmp, $_[0], 1);
318 system("cat \Q$tmpname\E | " . myout($archive,0))==0
319 or error "Can't write to archive";
320 system("rm -f -- \Q$tmpname\E");
323 # append diff to archive
324 # IN: diff archive name
325 # IN: newly created file name in archive
326 # IN: the real source file
328 # TODO: seems to be tricky. what to do?
329 # copyin of file which is already there may:
330 # * delete the original and copy only the new
331 # * just append the new hunks to the same file
332 # problems: may not be a valid diff, unmerged hunks
333 # * try to merge the two together
334 # ... but we do not want write patchutils again, right?
335 error "Copying files into diff not supported";
338 my ($archive,$name,$src)=@_;
340 # in case we are appending another diff, we have
341 # to delete/merge all the files
342 open(DEVNULL, ">/dev/null");
343 open I, myin($src).'|';
344 my ($srclist,$dstlist)=parse($archive, *DEVNULL, '', 0);
348 print("SRC: del $_\n");
351 print("DST: del $_\n");
355 # remove overwritten file
356 open I, myin($archive).'|';
357 rm ($archive, $name);
360 my $cmd1=myin("$src.diff");
361 my $cmd2=myout($archive,1);
362 system("$cmd1 | $cmd2")==0
363 or error "Can't write to archive";
367 if ($ARGV[0] eq 'list') {
368 open I, myin($ARGV[1]).'|';
371 } elsif ($ARGV[0] eq 'copyout') {
372 open I, myin($ARGV[1])."|";
373 copyout ($ARGV[2], $ARGV[3]);
375 } elsif ($ARGV[0] eq 'rm') {
376 open I, myin($ARGV[1])."|";
377 rm ($ARGV[1], $ARGV[2]);
379 } elsif ($ARGV[0] eq 'rmdir') {
381 } elsif ($ARGV[0] eq 'mkdir') {
383 } elsif ($ARGV[0] eq 'copyin') {
384 copyin ($ARGV[1], $ARGV[2], $ARGV[3]);