Avoid `$@' which is handled specially in
[bpt/emacs.git] / lib-src / rcs2log
CommitLineData
daecaf5d
GM
1#! /bin/sh
2
3# RCS to ChangeLog generator
4
5# Generate a change log prefix from RCS files (perhaps in the CVS repository)
6# and the ChangeLog (if any).
7# Output the new prefix to standard output.
8# You can edit this prefix by hand, and then prepend it to ChangeLog.
9
10# Ignore log entries that start with `#'.
11# Clump together log entries that start with `{topic} ',
12# where `topic' contains neither white space nor `}'.
13
14Help='The default FILEs are the files registered under the working directory.
15Options:
16
17 -c CHANGELOG Output a change log prefix to CHANGELOG (default ChangeLog).
18 -h HOSTNAME Use HOSTNAME in change log entries (default current host).
19 -i INDENT Indent change log lines by INDENT spaces (default 8).
20 -l LENGTH Try to limit log lines to LENGTH characters (default 79).
21 -R If no FILEs are given and RCS is used, recurse through working directory.
22 -r OPTION Pass OPTION to subsidiary log command.
23 -t TABWIDTH Tab stops are every TABWIDTH characters (default 8).
24 -u "LOGIN<tab>FULLNAME<tab>MAILADDR" Assume LOGIN has FULLNAME and MAILADDR.
25 -v Append RCS revision to file names in log lines.
26 --help Output help.
27 --version Output version number.
28
29Report bugs to <bug-gnu-emacs@gnu.org>.'
30
31Id='$Id: rcs2log,v 1.46 2001/01/03 12:04:06 gerd Exp $'
32
33# Copyright 1992, 93, 94, 95, 96, 97, 1998, 2001
34# Free Software Foundation, Inc.
35
36# This program is free software; you can redistribute it and/or modify
37# it under the terms of the GNU General Public License as published by
38# the Free Software Foundation; either version 2, or (at your option)
39# any later version.
40#
41# This program is distributed in the hope that it will be useful,
42# but WITHOUT ANY WARRANTY; without even the implied warranty of
43# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
44# GNU General Public License for more details.
45#
46# You should have received a copy of the GNU General Public License
47# along with this program; see the file COPYING. If not, write to the
48# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
49# Boston, MA 02111-1307, USA.
50
51Copyright='Copyright 1998 Free Software Foundation, Inc.
52This program comes with NO WARRANTY, to the extent permitted by law.
53You may redistribute copies of this program
54under the terms of the GNU General Public License.
55For more information about these matters, see the files named COPYING.
56Author: Paul Eggert <eggert@twinsun.com>'
57
58tab=' '
59nl='
60'
61
62# Parse options.
63
64# defaults
65: ${AWK=awk}
66: ${TMPDIR=/tmp}
67changelog=ChangeLog # change log file name
68datearg= # rlog date option
69hostname= # name of local host (if empty, will deduce it later)
70indent=8 # indent of log line
71length=79 # suggested max width of log line
72logins= # login names for people we know fullnames and mailaddrs of
73loginFullnameMailaddrs= # login<tab>fullname<tab>mailaddr triplets
74logTZ= # time zone for log dates (if empty, use local time)
75recursive= # t if we want recursive rlog
76revision= # t if we want revision numbers
77rlog_options= # options to pass to rlog
78tabwidth=8 # width of horizontal tab
79
80while :
81do
82 case $1 in
83 -c) changelog=${2?}; shift;;
84 -i) indent=${2?}; shift;;
85 -h) hostname=${2?}; shift;;
86 -l) length=${2?}; shift;;
87 -[nu]) # -n is obsolescent; it is replaced by -u.
88 case $1 in
89 -n) case ${2?}${3?}${4?} in
90 *"$tab"* | *"$nl"*)
91 echo >&2 "$0: -n '$2' '$3' '$4': tabs, newlines not allowed"
92 exit 1
93 esac
94 case $loginFullnameMailaddrs in
95 '') loginFullnameMailaddrs=$2$tab$3$tab$4;;
96 ?*) loginFullnameMailaddrs=$loginFullnameMailaddrs$nl$2$tab$3$tab$4
97 esac
98 shift; shift; shift;;
99 -u)
100 # If $2 is not tab-separated, use colon for separator.
101 case ${2?} in
102 *"$nl"*)
103 echo >&2 "$0: -u '$2': newlines not allowed"
104 exit 1;;
105 *"$tab"*)
106 t=$tab;;
107 *)
108 t=:
109 esac
110 case $2 in
111 *"$t"*"$t"*"$t"*)
112 echo >&2 "$0: -u '$2': too many fields"
113 exit 1;;
114 *"$t"*"$t"*)
115 ;;
116 *)
117 echo >&2 "$0: -u '$2': not enough fields"
118 exit 1
119 esac
120 case $loginFullnameMailaddrs in
121 '') loginFullnameMailaddrs=$2;;
122 ?*) loginFullnameMailaddrs=$loginFullnameMailaddrs$nl$2
123 esac
124 shift
125 esac
126 case $logins in
127 '') logins=$login;;
128 ?*) logins=$logins$nl$login
129 esac
130 ;;
131 -r)
132 case $rlog_options in
133 '') rlog_options=${2?};;
134 ?*) rlog_options=$rlog_options$nl${2?}
135 esac
136 shift;;
137 -R) recursive=t;;
138 -t) tabwidth=${2?}; shift;;
139 -v) revision=t;;
140 --version)
141 set $Id
142 rcs2logVersion=$3
143 echo >&2 "rcs2log (GNU Emacs) $rcs2logVersion$nl$Copyright"
144 exit 0;;
145 -*) echo >&2 "Usage: $0 [OPTION]... [FILE ...]$nl$Help"
146 case $1 in
147 --help) exit 0;;
148 *) exit 1
149 esac;;
150 *) break
151 esac
152 shift
153done
154
155month_data='
156 m[0]="Jan"; m[1]="Feb"; m[2]="Mar"
157 m[3]="Apr"; m[4]="May"; m[5]="Jun"
158 m[6]="Jul"; m[7]="Aug"; m[8]="Sep"
159 m[9]="Oct"; m[10]="Nov"; m[11]="Dec"
160'
161
162
163# Put rlog output into $rlogout.
164
165# If no rlog options are given,
166# log the revisions checked in since the first ChangeLog entry.
167# Since ChangeLog is only by date, some of these revisions may be duplicates of
168# what's already in ChangeLog; it's the user's responsibility to remove them.
169case $rlog_options in
170'')
171 if test -s "$changelog"
172 then
173 e='
174 /^[0-9]+-[0-9][0-9]-[0-9][0-9]/{
175 # ISO 8601 date
176 print $1
177 exit
178 }
179 /^... ... [ 0-9][0-9] [ 0-9][0-9]:[0-9][0-9]:[0-9][0-9] [0-9]+ /{
180 # old-fashioned date and time (Emacs 19.31 and earlier)
181 '"$month_data"'
182 year = $5
183 for (i=0; i<=11; i++) if (m[i] == $2) break
184 dd = $3
185 printf "%d-%02d-%02d\n", year, i+1, dd
186 exit
187 }
188 '
189 d=`$AWK "$e" <"$changelog"` || exit
190 case $d in
191 ?*) datearg="-d>$d"
192 esac
193 fi
194esac
195
196# Use TZ specified by ChangeLog local variable, if any.
197if test -s "$changelog"
198then
199 extractTZ='
200 /^.*change-log-time-zone-rule['"$tab"' ]*:['"$tab"' ]*"\([^"]*\)".*/{
201 s//\1/; p; q
202 }
203 /^.*change-log-time-zone-rule['"$tab"' ]*:['"$tab"' ]*t.*/{
204 s//UTC0/; p; q
205 }
206 '
207 logTZ=`tail "$changelog" | sed -n "$extractTZ"`
208 case $logTZ in
209 ?*) TZ=$logTZ; export TZ
210 esac
211fi
212
213# If CVS is in use, examine its repository, not the normal RCS files.
214if test ! -f CVS/Repository
215then
216 rlog=rlog
217 repository=
218else
219 rlog='cvs -q log'
220 repository=`sed 1q <CVS/Repository` || exit
221 test ! -f CVS/Root || CVSROOT=`cat <CVS/Root` || exit
222 case $CVSROOT in
223 *:/*)
224 # remote repository
225 ;;
226 *)
227 # local repository
228 case $repository in
229 /*) ;;
230 *) repository=${CVSROOT?}/$repository
231 esac
232 if test ! -d "$repository"
233 then
234 echo >&2 "$0: $repository: bad repository (see CVS/Repository)"
235 exit 1
236 fi
237 esac
238fi
239
240# Use $rlog's -zLT option, if $rlog supports it.
241case `$rlog -zLT 2>&1` in
242*' option'*) ;;
243*)
244 case $rlog_options in
245 '') rlog_options=-zLT;;
246 ?*) rlog_options=-zLT$nl$rlog_options
247 esac
248esac
249
250# With no arguments, examine all files under the RCS directory.
251case $# in
2520)
253 case $repository in
254 '')
255 oldIFS=$IFS
256 IFS=$nl
257 case $recursive in
258 t)
259 RCSdirs=`find . -name RCS -type d -print`
260 filesFromRCSfiles='s|,v$||; s|/RCS/|/|; s|^\./||'
261 files=`
262 {
263 case $RCSdirs in
264 ?*) find $RCSdirs \
265 -type f \
266 ! -name '*_' \
267 ! -name ',*,' \
268 ! -name '.*_' \
269 ! -name .rcsfreeze.log \
270 ! -name .rcsfreeze.ver \
271 -print
272 esac
273 find . -name '*,v' -print
274 } |
275 sort -u |
276 sed "$filesFromRCSfiles"
277 `;;
278 *)
279 files=
280 for file in RCS/.* RCS/* .*,v *,v
281 do
282 case $file in
283 RCS/. | RCS/.. | RCS/,*, | RCS/*_) continue;;
284 RCS/.rcsfreeze.log | RCS/.rcsfreeze.ver) continue;;
285 RCS/.\* | RCS/\* | .\*,v | \*,v) test -f "$file" || continue;;
286 RCS/*,v | RCS/.*,v) ;;
287 RCS/* | RCS/.*) test -f "$file" || continue
288 esac
289 case $files in
290 '') files=$file;;
291 ?*) files=$files$nl$file
292 esac
293 done
294 case $files in
295 '') exit 0
296 esac
297 esac
298 set x $files
299 shift
300 IFS=$oldIFS
301 esac
302esac
303
304logdir=$TMPDIR/rcs2log$$
305llogout=$logdir/l
306rlogout=$logdir/r
307trap exit 1 2 13 15
308trap "rm -fr $logdir 2>/dev/null" 0
309(umask 077 && exec mkdir $logdir) || exit
310
311case $datearg in
312?*) $rlog $rlog_options "$datearg" ${1+"$@"} >$rlogout;;
313'') $rlog $rlog_options ${1+"$@"} >$rlogout
314esac || exit
315
316
317# Get the full name of each author the logs mention, and set initialize_fullname
318# to awk code that initializes the `fullname' awk associative array.
319# Warning: foreign authors (i.e. not known in the passwd file) are mishandled;
320# you have to fix the resulting output by hand.
321
322initialize_fullname=
323initialize_mailaddr=
324
325case $loginFullnameMailaddrs in
326?*)
327 case $loginFullnameMailaddrs in
328 *\"* | *\\*)
329 sed 's/["\\]/\\&/g' >$llogout <<EOF || exit
330$loginFullnameMailaddrs
331EOF
332 loginFullnameMailaddrs=`cat $llogout`
333 esac
334
335 oldIFS=$IFS
336 IFS=$nl
337 for loginFullnameMailaddr in $loginFullnameMailaddrs
338 do
339 case $loginFullnameMailaddr in
340 *"$tab"*) IFS=$tab;;
341 *) IFS=:
342 esac
343 set x $loginFullnameMailaddr
344 login=$2
345 fullname=$3
346 mailaddr=$4
347 initialize_fullname="$initialize_fullname
348 fullname[\"$login\"] = \"$fullname\""
349 initialize_mailaddr="$initialize_mailaddr
350 mailaddr[\"$login\"] = \"$mailaddr\""
351 done
352 IFS=$oldIFS
353esac
354
355case $llogout in
356?*) sort -u -o $llogout <<EOF || exit
357$logins
358EOF
359esac
360output_authors='/^date: / {
361 if ($2 ~ /^[0-9]*[-\/][0-9][0-9][-\/][0-9][0-9]$/ && $3 ~ /^[0-9][0-9]:[0-9][0-9]:[0-9][0-9][-+0-9:]*;$/ && $4 == "author:" && $5 ~ /^[^;]*;$/) {
362 print substr($5, 1, length($5)-1)
363 }
364}'
365authors=`
366 $AWK "$output_authors" <$rlogout |
367 case $llogout in
368 '') sort -u;;
369 ?*) sort -u | comm -23 - $llogout
370 esac
371`
372case $authors in
373?*)
374 cat >$llogout <<EOF || exit
375$authors
376EOF
377 initialize_author_script='s/["\\]/\\&/g; s/.*/author[\"&\"] = 1/'
378 initialize_author=`sed -e "$initialize_author_script" <$llogout`
379 awkscript='
380 BEGIN {
381 alphabet = "abcdefghijklmnopqrstuvwxyz"
382 ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
383 '"$initialize_author"'
384 }
385 {
386 if (author[$1]) {
387 fullname = $5
388 if (fullname ~ /[0-9]+-[^(]*\([0-9]+\)$/) {
389 # Remove the junk from fullnames like "0000-Admin(0000)".
390 fullname = substr(fullname, index(fullname, "-") + 1)
391 fullname = substr(fullname, 1, index(fullname, "(") - 1)
392 }
393 if (fullname ~ /,[^ ]/) {
394 # Some sites put comma-separated junk after the fullname.
395 # Remove it, but leave "Bill Gates, Jr" alone.
396 fullname = substr(fullname, 1, index(fullname, ",") - 1)
397 }
398 abbr = index(fullname, "&")
399 if (abbr) {
400 a = substr($1, 1, 1)
401 A = a
402 i = index(alphabet, a)
403 if (i) A = substr(ALPHABET, i, 1)
404 fullname = substr(fullname, 1, abbr-1) A substr($1, 2) substr(fullname, abbr+1)
405 }
406
407 # Quote quotes and backslashes properly in full names.
408 # Do not use gsub; traditional awk lacks it.
409 quoted = ""
410 rest = fullname
411 for (;;) {
412 p = index(rest, "\\")
413 q = index(rest, "\"")
414 if (p) {
415 if (q && q<p) p = q
416 } else {
417 if (!q) break
418 p = q
419 }
420 quoted = quoted substr(rest, 1, p-1) "\\" substr(rest, p, 1)
421 rest = substr(rest, p+1)
422 }
423
424 printf "fullname[\"%s\"] = \"%s%s\"\n", $1, quoted, rest
425 author[$1] = 0
426 }
427 }
428 '
429
430 initialize_fullname=`
431 {
432 (getent passwd $authors) ||
433 (
434 cat /etc/passwd
435 for author in $authors
436 do NIS_PATH= nismatch $author passwd.org_dir
437 done
438 ypmatch $authors passwd
439 )
440 } 2>/dev/null |
441 $AWK -F: "$awkscript"
442 `$initialize_fullname
443esac
444
445
446# Function to print a single log line.
447# We don't use awk functions, to stay compatible with old awk versions.
448# `Log' is the log message (with \n replaced by \001).
449# `files' contains the affected files.
450printlogline='{
451
452 # Following the GNU coding standards, rewrite
453 # * file: (function): comment
454 # to
455 # * file (function): comment
456 if (Log ~ /^\([^)]*\): /) {
457 i = index(Log, ")")
458 files = files " " substr(Log, 1, i)
459 Log = substr(Log, i+3)
460 }
461
462 # If "label: comment" is too long, break the line after the ":".
463 sep = " "
464 if ('"$length"' <= '"$indent"' + 1 + length(files) + index(Log, SOH)) sep = "\n" indent_string
465
466 # Print the label.
467 printf "%s*%s:", indent_string, files
468
469 # Print each line of the log, transliterating \001 to \n.
470 while ((i = index(Log, SOH)) != 0) {
471 logline = substr(Log, 1, i-1)
472 if (logline ~ /[^'"$tab"' ]/) {
473 printf "%s%s\n", sep, logline
474 } else {
475 print ""
476 }
477 sep = indent_string
478 Log = substr(Log, i+1)
479 }
480}'
481
482# Pattern to match the `revision' line of rlog output.
483rlog_revision_pattern='^revision [0-9]+\.[0-9]+(\.[0-9]+\.[0-9]+)*(['"$tab"' ]+locked by: [^'"$tab"' $,.0-9:;@]*[^'"$tab"' $,:;@][^'"$tab"' $,.0-9:;@]*;)?['"$tab"' ]*$'
484
485case $hostname in
486'')
487 hostname=`(
488 hostname || uname -n || uuname -l || cat /etc/whoami
489 ) 2>/dev/null` || {
490 echo >&2 "$0: cannot deduce hostname"
491 exit 1
492 }
493
494 case $hostname in
495 *.*) ;;
496 *)
497 domainname=`(domainname) 2>/dev/null` &&
498 case $domainname in
499 *.*) hostname=$hostname.$domainname
500 esac
501 esac
502esac
503
504
505# Process the rlog output, generating ChangeLog style entries.
506
507# First, reformat the rlog output so that each line contains one log entry.
508# Transliterate \n to \001 so that multiline entries fit on a single line.
509# Discard irrelevant rlog output.
510$AWK <$rlogout '
511 BEGIN { repository = "'"$repository"'" }
512 /^RCS file:/ {
513 if (repository != "") {
514 filename = $3
515 if (substr(filename, 1, length(repository) + 1) == repository "/") {
516 filename = substr(filename, length(repository) + 2)
517 }
518 if (filename ~ /,v$/) {
519 filename = substr(filename, 1, length(filename) - 2)
520 }
521 if (filename ~ /(^|\/)Attic\/[^\/]*$/) {
522 i = length(filename)
523 while (substr(filename, i, 1) != "/") i--
524 filename = substr(filename, 1, i - 6) substr(filename, i + 1)
525 }
526 }
527 rev = "?"
528 }
529 /^Working file:/ { if (repository == "") filename = $3 }
530 /'"$rlog_revision_pattern"'/, /^(-----------*|===========*)$/ {
531 line = $0
532 if (line ~ /'"$rlog_revision_pattern"'/) {
533 rev = $2
534 next
535 }
536 if (line ~ /^date: [0-9][- +\/0-9:]*;/) {
537 date = $2
538 if (date ~ /\//) {
539 # This is a traditional RCS format date YYYY/MM/DD.
540 # Replace "/"s with "-"s to get ISO format.
541 newdate = ""
542 while ((i = index(date, "/")) != 0) {
543 newdate = newdate substr(date, 1, i-1) "-"
544 date = substr(date, i+1)
545 }
546 date = newdate date
547 }
548 time = substr($3, 1, length($3) - 1)
549 author = substr($5, 1, length($5)-1)
550 printf "%s %s %s %s %s %c", filename, rev, date, time, author, 1
551 rev = "?"
552 next
553 }
554 if (line ~ /^branches: /) { next }
555 if (line ~ /^(-----------*|===========*)$/) { print ""; next }
556 if (line == "Initial revision" || line ~ /^file .+ was initially added on branch .+\.$/) {
557 line = "New file."
558 }
559 printf "%s%c", line, 1
560 }
561' |
562
563# Now each line is of the form
564# FILENAME REVISION YYYY-MM-DD HH:MM:SS[+-TIMEZONE] AUTHOR \001LOG
565# where \001 stands for a carriage return,
566# and each line of the log is terminated by \001 instead of \n.
567# Sort the log entries, first by date+time (in reverse order),
568# then by author, then by log entry, and finally by file name and revision
569# (just in case).
570sort +2 -4r +4 +0 |
571
572# Finally, reformat the sorted log entries.
573$AWK '
574 BEGIN {
575 logTZ = "'"$logTZ"'"
576 revision = "'"$revision"'"
577
578 # Some awk variants do not understand "\001", so we have to
579 # put the char directly in the file.
580 SOH="\ 1" # <-- There is a single SOH (octal code 001) here.
581
582 # Initialize the fullname and mailaddr associative arrays.
583 '"$initialize_fullname"'
584 '"$initialize_mailaddr"'
585
586 # Initialize indent string.
587 indent_string = ""
588 i = '"$indent"'
589 if (0 < '"$tabwidth"')
590 for (; '"$tabwidth"' <= i; i -= '"$tabwidth"')
591 indent_string = indent_string "\t"
592 while (1 <= i--)
593 indent_string = indent_string " "
594 }
595
596 {
597 newlog = substr($0, 1 + index($0, SOH))
598
599 # Ignore log entries prefixed by "#".
600 if (newlog ~ /^#/) { next }
601
602 if (Log != newlog || date != $3 || author != $5) {
603
604 # The previous log and this log differ.
605
606 # Print the old log.
607 if (date != "") '"$printlogline"'
608
609 # Logs that begin with "{clumpname} " should be grouped together,
610 # and the clumpname should be removed.
611 # Extract the new clumpname from the log header,
612 # and use it to decide whether to output a blank line.
613 newclumpname = ""
614 sep = "\n"
615 if (date == "") sep = ""
616 if (newlog ~ /^\{[^'"$tab"' }]*}['"$tab"' ]/) {
617 i = index(newlog, "}")
618 newclumpname = substr(newlog, 1, i)
619 while (substr(newlog, i+1) ~ /^['"$tab"' ]/) i++
620 newlog = substr(newlog, i+1)
621 if (clumpname == newclumpname) sep = ""
622 }
623 printf sep
624 clumpname = newclumpname
625
626 # Get ready for the next log.
627 Log = newlog
628 if (files != "")
629 for (i in filesknown)
630 filesknown[i] = 0
631 files = ""
632 }
633 if (date != $3 || author != $5) {
634 # The previous date+author and this date+author differ.
635 # Print the new one.
636 date = $3
637 time = $4
638 author = $5
639
640 zone = ""
641 if (logTZ && ((i = index(time, "-")) || (i = index(time, "+"))))
642 zone = " " substr(time, i)
643
644 # Print "date[ timezone] fullname <email address>".
645 # Get fullname and email address from associative arrays;
646 # default to author and author@hostname if not in arrays.
647 if (fullname[author])
648 auth = fullname[author]
649 else
650 auth = author
651 printf "%s%s %s ", date, zone, auth
652 if (mailaddr[author])
653 printf "<%s>\n\n", mailaddr[author]
654 else
655 printf "<%s@%s>\n\n", author, "'"$hostname"'"
656 }
657 if (! filesknown[$1]) {
658 filesknown[$1] = 1
659 if (files == "") files = " " $1
660 else files = files ", " $1
661 if (revision && $2 != "?") files = files " " $2
662 }
663 }
664 END {
665 # Print the last log.
666 if (date != "") {
667 '"$printlogline"'
668 printf "\n"
669 }
670 }
671' &&
672
673
674# Exit successfully.
675
676exec rm -fr $logdir
677
678# Local Variables:
679# tab-width:4
680# End: