Wiggle 0.6 - first release
[wiggle/upstream.git] / p
blob755e0a2e49bec2cece8a898e96de4208bc3b71f3
1 #!/bin/bash
3 # patch management
5 # Copyright (C) 2003 Neil Brown <neilb@cse.unsw.edu.au>
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 # Author: Neil Brown
23 # Email: <neilb@cse.unsw.edu.au>
24 # Paper: Neil Brown
25 # School of Computer Science and Engineering
26 # The University of New South Wales
27 # Sydney, 2052
28 # Australia
31 # metadata is in .patches
32 # there is:
33 # files: list of all files checked out
34 # name: name of current patch
35 # status: status of current patch
36 # notes: notes on current patch
37 # applied/ patches applied nnn-name
38 # removed/ patches removed nnn-name
39 # included/ patches that have been included upstream
40 # patch: a recent copy of the 'current' patch
41 # get-version: a script which will report the version number of the base dist
42 # dest/ symlink to directory to publish snapshots to
43 # mail/ composed mail messages ready for sending
44 # maintainer who to email patches to (Linus etc)
45 # cc who to CC patches to: prefix address
47 # the nnn in names in applied and removed are sequence numbers
48 # whenever we add a file we choose one more than the highest used number
49 # patch files contain then name implicitly and start with
50 # Status: status
51 # then a blank line, normally a one line description, another blank, and more detail.
55 # Todo - auto bk pull:
56 # bk pull
57 # bk export -t patch -r DEVEL, > /tmp/apatch
58 # bk tag DEVEL
59 # while p open last && p discard ; do : ; done
60 # p clean
61 # patch -p1 -f < /tmp/apatch
63 find_home()
65 # walk up directory tree until a .patches directory
66 # is found.
67 # set OrigDir to name of where we were .. not dots.
68 OrigDir=
69 dir=`pwd`
70 while [ ! -d .patches -a " $dir" != " /" ]
72 base=${dir##*/}
73 base=${base#/}
74 dir=${dir%/*}
75 case $dir in
76 "" ) dir=/
77 esac
78 OrigDir=$base/$OrigDir
79 cd ..
80 done
81 test -d .patches
84 get_meta()
86 name=`cat .patches/name 2> /dev/null`
87 status=`cat .patches/status 2> /dev/null`
90 forget_one()
92 if cmp -s "$1" "$1~current~" && cmp -s "$1" "$1~orig~"
93 then
94 rm -f "$1~current~" "$1~orig~"
95 chmod -w "$1"
96 else
97 echo >&2 "ERROR $1 doesn't match original"
102 snap_one()
104 cp "$1" "$1~snapshot~"
107 snap_diff()
109 diff -u "$1" "$1~snapshot~"
111 snap_back()
113 cp "$1~snapshot~" "$1"
116 check_out()
118 file=$1
119 file=${file#./}
120 [ -f $file ] || >> $file
121 if [ -f $file ]
122 then
123 if [ ! -f "$file~orig~" ] ; then
124 mv "$file" "$file~orig~"
125 cp "$file~orig~" "$file"
126 echo $file >> .patches/files
127 sort -o .patches/files .patches/files
128 chmod u+w "$file"
130 if [ ! -f "$file~current~" ] ; then
131 mv "$file" "$file~current~"
132 cp "$file~current~" "$file"
134 else
135 echo >&2 Cannot checkout $file
139 all_files()
141 >> .patches/files
142 while read file
143 do eval $1 $file
144 done < .patches/files
147 diff_one()
149 if cmp -s "$1~current~" "$1"
150 then :
151 else
152 echo
153 echo "diff ./$1~current~ ./$1"
154 diff --show-c-function -u ./$1~current~ ./$1
158 diff_one_orig()
160 if cmp -s "$1~orig~" "$1"
161 then :
162 else
163 echo
164 echo "diff ./$1~orig~ ./$1"
165 diff --show-c-function -u ./$1~orig~ ./$1
169 commit_one()
171 rm -f "$1~current~"
172 mv "$1" "$1~current~"
173 cp "$1~current~" $1
174 chmod u+w $1
177 discard_one()
179 rm -f "$1"
180 cp "$1~current~" $1
181 chmod u+w $1
184 swap_one()
186 mv "$1" "$1.tmp"
187 mv "$1~current~" "$1"
188 mv "$1.tmp" "$1~current~"
191 make_diff()
194 [ -s .patches/status ] && echo "Status: `cat .patches/status`"
195 echo
196 [ -s .patches/notes ] && { cat .patches/notes ; echo; }
197 all_files diff_one > .patches/tmp
198 echo " ----------- Diffstat output ------------"
199 diffstat -p0 2> /dev/null < .patches/tmp
200 cat .patches/tmp
201 [ -s .patches/tmp ] || rm .patches/patch
202 rm .patches/tmp
203 } > .patches/patch
206 save_patch()
208 dir=.patches/$1
209 name=$2
210 # move .patches/patch to $dir/nnn$name
211 #for some new nnn
212 [ -d $dir ] || mkdir $dir || exit 1
213 largest=`ls $dir | sed -n -e 's/^\([0-9][0-9][0-9]\).*/\1/p' | sort -n | tail -1`
214 if [ "0$largest" -eq 999 ]
215 then echo >&2 'ARRG - too many patches!' ; exit 1
217 new=`expr "0$largest" + 1001`
218 new=${new#1}
219 mv .patches/patch $dir/$new$name
222 find_prefix()
224 # set "prefix" to number for -pn by looking at first file in given patch.
225 file=`lsdiff $1 | head -1`
226 orig=$file
227 prefix=0
228 while [ -n "$file" -a ! -f "$file" ]
230 file=`expr "$file" : '[^/]*/\(.*\)'`
231 prefix=`expr $prefix + 1`
232 done
233 if [ -z "$file" ]
234 then echo "Cannot find $orig" >&2 ; exit 1;
236 if [ " $orig" != " $file" ]
237 then
238 echo "Found $orig as $file - prefix $prefix"
242 extract_notes()
244 # remove first line, Status: line, leading blanks,
245 # everything from ' *---' and trailing blanks
246 awk '
247 BEGIN { head= 1; blanks=0 ; }
248 head == 1 && ( $1 == "Status:" || $0 == "" ) {
249 next;
251 { head = 0; }
252 $0 == "" { blanks++; next; }
253 $0 ~ /^ *---/ { exit }
254 { while (blanks > 0) {
255 blanks--; print "";
257 print $0;
259 ' $1
263 if [ $# -eq 0 ]
264 then
265 echo >&2 'Usage: p [help|co|make|discard|commit|status|name|...] args'
266 exit 1
268 cmd=$1
269 shift
271 if [ " $cmd" = " help" ] || find_home
272 then :
273 else echo >&2 "p $cmd: cannot find .patches directory"
274 exit 1
277 case $cmd in
278 co )
279 if [ $# -ne 1 ] ; then
280 echo >&2 Usage: p co file; exit 1
282 file=$1
283 if [ ! -f "$OrigDir$file" ]
284 then
285 echo >&2 "p co: file $file not found"; exit 1;
287 check_out "$OrigDir$file"
290 make | view )
291 case $1 in
292 "" )
293 make_diff
294 if [ -s .patches/patch ] ; then
295 pfile=.patches/patch
296 else
297 echo >&2 "No current patch" ; exit 1;
301 */* ) pfile=$1;;
302 * ) pfile=`echo .patches/[ra][ep][mp]*/*$1*`
303 esac
304 if [ ! -f "$pfile" ]
305 then echo >&2 "Cannot find unique patch '$1' - found: $pfile"; exit 1;
307 ${PAGER-less} $pfile;
310 all )
311 all_files diff_one_orig
313 status | name )
314 case $# in
315 1 )
316 get_meta
317 if [ $cmd = name ] ; then
318 if [ -n "$name" ]; then
319 echo "changing name from '$name' to '$1'"
320 else
321 echo "Setting name to '$1'"
323 echo "$1" > .patches/name
325 if [ $cmd = status ] ; then
326 if [ -n "$status" ]; then
327 echo "changing status from '$status' to '$1'"
328 else
329 echo "Setting status to '$1'"
331 echo "$1" > .patches/status
335 get_meta
336 echo -n "Name ($name)? " ; read name
337 echo -n "Status ($status)? " ; read status
338 [ -n "$name" ] && { echo $name > .patches/name ; }
339 [ -n "$status" ] && { echo $status > .patches/status ; }
342 echo "Usage: p $cmd [new-$cmd]"; exit 1;
343 esac
345 note* )
346 >> .patches/notes
347 ${EDITOR:-vi} .patches/notes
349 discard|commit )
350 make_diff
351 if [ -s .patches/patch ]
352 then :
353 else echo >&2 No patch to $cmd ; exit 1
355 if [ -s .patches/to-resolv ]
356 then echo "Please resolve outstanding conflicts first with 'p resolve'"
357 exit 1
359 get_meta
360 if [ -z "$name" ] ; then
361 echo -n "Name? " ; read name
362 if [ -z "$name" ] ; then
363 echo >&2 "No current name, please set with 'p name'"
364 exit 1;
366 echo $name > .patches/name
368 if [ -z "$status" ] ; then
369 echo -n "Status? " ; read status
370 if [ -z "$status" ] ; then
371 echo >&2 "No current status, please set with 'p status'"
372 exit 1;
374 echo $status > .patches/status
376 if [ -s .patches/notes ]
377 then :
378 else
379 { echo "Title...."
380 echo
381 echo "Description..."
382 echo
383 echo "====Do Not Remove===="
384 cat .patches/patch
385 } > .patches/notes
386 ${EDITOR-vi} .patches/notes
387 mv .patches/notes .patches/tmp
388 sed '/^====Do Not Remove====/,$d' .patches/tmp > .patches/notes
389 rm .patches/tmp
391 make_diff
393 if [ $cmd = commit ] ; then
394 save_patch applied "$name"
395 echo Saved as $new$name
396 all_files commit_one
397 else
398 save_patch removed "$name"
399 echo Saved as $new$name
400 all_files discard_one
402 rm -f .patches/name .patches/status .patches/notes
405 purge )
406 make_diff
407 mv .patches/patch .patches/last-purge
408 all_files discard_one
409 rm -f .patches/name .patches/status .patches/notes
411 open )
412 make_diff
413 get_meta
414 if [ -s .patches/patch ]
415 then
416 echo >&2 Patch $name already open - please commit; exit 1;
418 if [ $# -eq 0 ]
419 then
420 echo "Available patches are:"
421 ls .patches/applied
422 exit 0
424 if [ $# -ne 1 ]
425 then echo >&2 "Usage: p open patchname" ; exit 1
427 if [ " $1" = " last" ]
428 then
429 pfile=`ls -d .patches/applied/[0-9]* | tail -1`
430 else
431 pfile=`echo .patches/applied/*$1*`
433 if [ ! -f "$pfile" ]
434 then echo >&2 "Cannot find unique patch '$1' - found: $pfile"; exit 1
436 # lets see if it applies cleanly
437 if patch -s --fuzz=0 --dry-run -R -f -p0 < "$pfile"
438 then echo Ok, it seems to apply
439 else echo >&2 "Sorry, that patch doesn't apply" ; exit 1
441 # lets go for it ...
442 patch --fuzz=0 -R -f -p0 < "$pfile"
443 all_files swap_one
444 sed -n -e '2q' -e 's/^Status: *//p' $pfile > .patches/status
445 base=${pfile##*/[0-9][0-9][0-9]}
446 [ -s .patches/name ] || echo $base > .patches/name
447 extract_notes $pfile >> .patches/notes
448 mv $pfile .patches/patch
451 included )
452 force=
453 if [ " $1" = " -f" ] ; then
454 force=yes; shift
456 make_diff; get_meta
457 if [ -s .patches/patch ]
458 then
459 echo >&2 Patch $name already open, please commit; exit 1;
461 if [ $# -eq 0 ]
462 then
463 echo "Unapplied patches are:"
464 ls .patches/removed
465 exit 0;
467 if [ $# -ne 1 ]
468 then
469 echo >&2 "Usage: p included patchname"; exit 1
471 case $1 in
472 last ) pfile=`ls -d .patches/removed/[0-9]* | tail -1` ;;
473 */* ) echo >&2 "Only local patches can have been included"; exit 1 ;;
474 *) pfile=`echo .patches/removed/*$1*`
475 esac
476 if [ ! -f "$pfile" ]
477 then echo >&2 "Cannot find unique patch '$1' - found $pfile"; exit 1
479 echo "Using $pfile..."
481 # make sure patch applies in reverse
482 if patch -s --fuzz=0 --dry-run -f -p0 -R < "$pfile"
483 then echo "Yep, that seems to be included"
484 elif [ -n "$force" ]
485 then echo "It doesn't apply reverse-out cleanly, but you asked for it..."
486 else echo >&2 "Sorry, patch cannot be removed"; exit 1
488 mv "$pfile" .patches/patch
489 name=${pfile##*/[0-9][0-9][0-9]}
490 save_patch included $name
491 echo "Moved to $new$name"
493 list )
494 echo "Applied patches are:"
495 ls .patches/applied
497 echo "Unapplied patches are:"
498 ls .patches/removed
499 exit 0
501 apply )
502 force= append=
503 if [ " $1" = " -f" ]; then
504 force=yes; shift
506 if [ " $1" = " -a" ]; then
507 append=yes; shift
509 make_diff
510 get_meta
511 if [ -s .patches/patch -a -z "$append" ]
512 then
513 echo >&2 Patch $name already open - please commit ; exit 1;
515 if [ $# -eq 0 ]
516 then
517 echo "Unapplied patches are:"
518 ls .patches/removed
519 exit 0
521 if [ $# -ne 1 ]
522 then echo >&2 "Usage: p apply patchname"; exit 1
524 case $1 in
525 last ) pfile=`ls -d .patches/removed/[0-9]* | tail -1` ; echo last is "$pfile";;
526 */* ) pfile=$1 ;;
527 * ) pfile=`echo .patches/removed/*$1*`
528 esac
529 if [ ! -f "$pfile" ]
530 then echo >&2 "Cannot find unique patch '$1' - found: $pfile"; exit 1
532 find_prefix "$pfile"
533 lsdiff --strip=$prefix "$pfile" | grep -v 'file.*changed' | while read a b
534 do check_out $a
535 done
536 # lets see if it applies cleanly
537 if patch -s --fuzz=0 --dry-run -f -p$prefix < "$pfile"
538 then echo OK, it seems to apply
539 elif [ -n "$force" ]
540 then echo "It doesn't apply cleanly, but you asked for it...."
541 echo "Saving original at .patches/last-applied"
542 cp $pfile .patches/last-applied
543 else echo >&2 "Sorry, patch doesn't apply"; exit 1
545 # lets go for it ...
546 patch --fuzz=0 -f -p$prefix < "$pfile" | tee .patches/tmp
547 sed -n -e '2q' -e 's/^Status: *//p' $pfile > .patches/status
548 base=${pfile##*/}
549 base=${base##[0-9][0-9][0-9]}
550 base=${base##patch-?-}
551 [ -s .patches/name ] || echo $base > .patches/name
552 extract_notes $pfile >> .patches/notes
554 sed -n -e 's/.*saving rejects to file \(.*\).rej/\1/p' .patches/tmp |
555 while read file
556 do echo Wiggling $file.rej into place
557 rm -f $file.porig
558 wiggle --replace --merge $file $file.rej ||
559 echo $file >> .patches/to-resolve
560 done
562 case $pfile in
563 .patches/removed/* )
564 mv $pfile .patches/patch
565 esac
568 publish )
569 name=`date -u +%Y-%m-%d:%H`
570 if [ -d .patches/dest ]
571 then : good
572 else echo >&2 No destination specified at .patches/dest ; exit 1;
574 if [ -d .patches/dest/$name ]
575 then
576 echo >&2 $name already exists ; exit 1
578 target=.patches/dest/$name
579 mkdir $target
580 if [ -f .patches/get-version ] ;
581 then ./.patches/get-version > $target/version
583 [ -f .config ] && cp .config $target
584 cp .patches/applied/* $target
585 mkdir $target/misc
586 cp 2> /dev/null .patches/removed/* $target/misc || rmdir $target/misc
587 chmod -R a+rX $target
588 all_files diff_one_orig > $target/patch-all-$name
589 cd $target
590 echo Published at `/bin/pwd`
592 clean )
593 all_files forget_one
594 > .patches/files
596 openall )
597 while p open last && p discard ; do : ; done
599 snapshot )
600 all_files snap_one
602 snapdiff )
603 all_files snap_diff
605 snapback )
606 all_files snap_back
608 resolve )
609 if [ ! -s .patches/resolving ]
610 then sort -u .patches/to-resolve > .patches/resolving ; > .patches/to-resolve
612 if [ ! -s .patches/resolving ]
613 then echo "Nothing to resolve" ; exit 0;
615 echo "Resolving: " ; cat .patches/resolving
616 for file in `cat .patches/resolving`
618 ${EDITOR:-vi} $file
619 rm -f $file.porig
620 wiggle --replace --merge $file ||
621 echo $file >> .patches/to-resolve
622 done
623 > .patches/resolving
625 pull )
626 cd .patches/SOURCE && bk pull
628 update )
629 p openall && p clean &&
630 (cd .patches/SOURCE ; bk export -tpatch -rLATEST, ) > .patches/imported-patch &&
631 patch --dry-run -f -p1 < .patches/imported-patch &&
632 patch -f -p1 < .patches/imported-patch &&
633 ( rm .patches/imported-patch ; cd .patches/SOURCE ; bk tag LATEST )
636 premail )
637 # Convert some applied patches into email messages.
638 # Select patches that start with $1. Look in .patches/cc for who to Cc: to
639 rmdir .patches/mail 2>/dev/null
640 if [ -d .patches/mail ] ; then
641 echo >&2 There is already some email - run "email" or "nomail"
642 ls .patches/mail
643 exit 1;
645 mkdir .patches/mail
646 if [ ! -s .patches/maintainer ] ; then
647 echo "No maintainer - please add one"
648 exit 1;
650 if [ ! -s .patches/owner ] ; then
651 echo "Your address and other headers must be in .patches/owner"
652 exit 1;
654 cnt=$(ls .patches/applied/???${1}* | wc -l)
655 cnt=$(echo $cnt) # discard spaces
656 this=1
657 for patch in .patches/applied/???${1}*
660 sprefix=
661 cat .patches/owner
662 echo "To: `cat .patches/maintainer`"
663 if [ -s .patches/cc ] ; then
664 while read word prefix addr
665 do if [ " $word" = " $1" ] ; then
666 echo "Cc: $addr"
667 sprefix="$prefix - "
669 done < .patches/cc
671 head=`sed -e '/^Status/d' -e '/^$/d' -e q $patch`
672 if [ $cnt = 1 ]
673 then
674 echo "Subject: [PATCH] $sprefix $head"
675 else
676 echo "Subject: [PATCH] $sprefix$this of $cnt - $head"
678 echo
679 echo '### Comments for ChangeSet'
680 sed -e '1,/^[^S]/d' $patch
681 } > .patches/mail/${patch#.patches/applied/}
682 this=$(expr $this + 1)
683 done
684 ls .patches/mail
687 nomail )
688 echo "Removing .patches/mail directory"
689 rm -rf .patches/mail
692 email )
693 PATH=/usr/lib:/usr/sbin:$PATH
694 for i in .patches/mail/*
696 if [ -f "$i" ]
697 then
698 echo Sending $i.
699 sendmail -t < $i && rm $i
701 done
703 help )
704 helpfile=$0.help
705 if [ ! -f $helpfile ]
706 then echo >&2 $helpfile not found: no help available ; exit 2;
708 if [ -z "$1" ] ; then
709 echo
710 sed -n -e '/^ /p' -e '/^[^ ]/q' $helpfile
711 echo
712 echo "Available help topics are:"
713 sed -n '/^[^ ]/p' $helpfile | sort | column
714 else
715 echo
716 awk '$0 ~ /^[^ ]/ && printed {doprint=0; printed=0}
717 doprint && $0 !~ /^[^ ]/ {print; printed=1;}
718 $0 == "'$1'" {doprint=1; found=1}
719 END { if (!found) print "No help available for '$1'"; }
720 ' $helpfile
721 echo
725 echo >&2 "p $cmd - unknown command - try 'p help'"; exit 1;
726 esac
727 exit 0;