(SYSV): Don't define on Solaris 2.
[emacs.git] / lib-src / rcs2log
blobf1bd89dc715b21e23977e8d8b5bce7683c32ec0b
1 #! /bin/sh
3 # RCS to ChangeLog generator
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.
10 # Ignore log entries that start with `#'.
11 # Clump together log entries that start with `{topic} ',
12 # where `topic' contains neither white space nor `}'.
14 Help='The default FILEs are the files registered under the working directory.
15 Options:
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.
29 Report bugs to <bug-gnu-emacs@gnu.org>.'
31 Id='$Id: rcs2log,v 1.46 2001/01/03 12:04:06 gerd Exp $'
33 # Copyright 1992, 93, 94, 95, 96, 97, 1998, 2001
34 # Free Software Foundation, Inc.
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.
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.
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.
51 Copyright='Copyright 1998 Free Software Foundation, Inc.
52 This program comes with NO WARRANTY, to the extent permitted by law.
53 You may redistribute copies of this program
54 under the terms of the GNU General Public License.
55 For more information about these matters, see the files named COPYING.
56 Author: Paul Eggert <eggert@twinsun.com>'
58 tab=' '
59 nl='
62 # Parse options.
64 # defaults
65 : ${AWK=awk}
66 : ${TMPDIR=/tmp}
67 changelog=ChangeLog # change log file name
68 datearg= # rlog date option
69 hostname= # name of local host (if empty, will deduce it later)
70 indent=8 # indent of log line
71 length=79 # suggested max width of log line
72 logins= # login names for people we know fullnames and mailaddrs of
73 loginFullnameMailaddrs= # login<tab>fullname<tab>mailaddr triplets
74 logTZ= # time zone for log dates (if empty, use local time)
75 recursive= # t if we want recursive rlog
76 revision= # t if we want revision numbers
77 rlog_options= # options to pass to rlog
78 tabwidth=8 # width of horizontal tab
80 while :
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;;
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"*)
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
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
153 done
155 month_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"
163 # Put rlog output into $rlogout.
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.
169 case $rlog_options in
171 if test -s "$changelog"
172 then
174 /^[0-9]+-[0-9][0-9]-[0-9][0-9]/{
175 # ISO 8601 date
176 print $1
177 exit
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
189 d=`$AWK "$e" <"$changelog"` || exit
190 case $d in
191 ?*) datearg="-d>$d"
192 esac
194 esac
196 # Use TZ specified by ChangeLog local variable, if any.
197 if test -s "$changelog"
198 then
199 extractTZ='
200 /^.*change-log-time-zone-rule['"$tab"' ]*:['"$tab"' ]*"\([^"]*\)".*/{
201 s//\1/; p; q
203 /^.*change-log-time-zone-rule['"$tab"' ]*:['"$tab"' ]*t.*/{
204 s//UTC0/; p; q
207 logTZ=`tail "$changelog" | sed -n "$extractTZ"`
208 case $logTZ in
209 ?*) TZ=$logTZ; export TZ
210 esac
213 # If CVS is in use, examine its repository, not the normal RCS files.
214 if test ! -f CVS/Repository
215 then
216 rlog=rlog
217 repository=
218 else
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
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
237 esac
240 # Use $rlog's -zLT option, if $rlog supports it.
241 case `$rlog -zLT 2>&1` in
242 *' option'*) ;;
244 case $rlog_options in
245 '') rlog_options=-zLT;;
246 ?*) rlog_options=-zLT$nl$rlog_options
247 esac
248 esac
250 # With no arguments, examine all files under the RCS directory.
251 case $# in
253 case $repository in
255 oldIFS=$IFS
256 IFS=$nl
257 case $recursive in
259 RCSdirs=`find . -name RCS -type d -print`
260 filesFromRCSfiles='s|,v$||; s|/RCS/|/|; s|^\./||'
261 files=`
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
275 sort -u |
276 sed "$filesFromRCSfiles"
279 files=
280 for file in RCS/.* RCS/* .*,v *,v
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
302 esac
304 logdir=$TMPDIR/rcs2log$$
305 llogout=$logdir/l
306 rlogout=$logdir/r
307 trap exit 1 2 13 15
308 trap "rm -fr $logdir 2>/dev/null" 0
309 (umask 077 && exec mkdir $logdir) || exit
311 case $datearg in
312 ?*) $rlog $rlog_options "$datearg" ${1+"$@"} >$rlogout;;
313 '') $rlog $rlog_options ${1+"$@"} >$rlogout
314 esac || exit
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.
322 initialize_fullname=
323 initialize_mailaddr=
325 case $loginFullnameMailaddrs in
327 case $loginFullnameMailaddrs in
328 *\"* | *\\*)
329 sed 's/["\\]/\\&/g' >$llogout <<EOF || exit
330 $loginFullnameMailaddrs
332 loginFullnameMailaddrs=`cat $llogout`
333 esac
335 oldIFS=$IFS
336 IFS=$nl
337 for loginFullnameMailaddr in $loginFullnameMailaddrs
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
353 esac
355 case $llogout in
356 ?*) sort -u -o $llogout <<EOF || exit
357 $logins
359 esac
360 output_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)
365 authors=`
366 $AWK "$output_authors" <$rlogout |
367 case $llogout in
368 '') sort -u;;
369 ?*) sort -u | comm -23 - $llogout
370 esac
372 case $authors in
374 cat >$llogout <<EOF || exit
375 $authors
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"'
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)
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)
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)
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
420 quoted = quoted substr(rest, 1, p-1) "\\" substr(rest, p, 1)
421 rest = substr(rest, p+1)
424 printf "fullname[\"%s\"] = \"%s%s\"\n", $1, quoted, rest
425 author[$1] = 0
430 initialize_fullname=`
432 (getent passwd $authors) ||
434 cat /etc/passwd
435 for author in $authors
436 do NIS_PATH= nismatch $author passwd.org_dir
437 done
438 ypmatch $authors passwd
440 } 2>/dev/null |
441 $AWK -F: "$awkscript"
442 `$initialize_fullname
443 esac
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.
450 printlogline='{
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)
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
466 # Print the label.
467 printf "%s*%s:", indent_string, files
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 ""
477 sep = indent_string
478 Log = substr(Log, i+1)
482 # Pattern to match the `revision' line of rlog output.
483 rlog_revision_pattern='^revision [0-9]+\.[0-9]+(\.[0-9]+\.[0-9]+)*(['"$tab"' ]+locked by: [^'"$tab"' $,.0-9:;@]*[^'"$tab"' $,:;@][^'"$tab"' $,.0-9:;@]*;)?['"$tab"' ]*$'
485 case $hostname in
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
494 case $hostname in
495 *.*) ;;
497 domainname=`(domainname) 2>/dev/null` &&
498 case $domainname in
499 *.*) hostname=$hostname.$domainname
500 esac
501 esac
502 esac
505 # Process the rlog output, generating ChangeLog style entries.
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)
518 if (filename ~ /,v$/) {
519 filename = substr(filename, 1, length(filename) - 2)
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)
527 rev = "?"
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
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)
546 date = newdate date
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
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."
559 printf "%s%c", line, 1
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).
570 sort +2 -4r +4 +0 |
572 # Finally, reformat the sorted log entries.
573 $AWK '
574 BEGIN {
575 logTZ = "'"$logTZ"'"
576 revision = "'"$revision"'"
578 # Some awk variants do not understand "\001", so we have to
579 # put the char directly in the file.
580 SOH="\x01" # <-- There is a single SOH (octal code 001) here.
582 # Initialize the fullname and mailaddr associative arrays.
583 '"$initialize_fullname"'
584 '"$initialize_mailaddr"'
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 " "
597 newlog = substr($0, 1 + index($0, SOH))
599 # Ignore log entries prefixed by "#".
600 if (newlog ~ /^#/) { next }
602 if (Log != newlog || date != $3 || author != $5) {
604 # The previous log and this log differ.
606 # Print the old log.
607 if (date != "") '"$printlogline"'
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 = ""
623 printf sep
624 clumpname = newclumpname
626 # Get ready for the next log.
627 Log = newlog
628 if (files != "")
629 for (i in filesknown)
630 filesknown[i] = 0
631 files = ""
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
640 zone = ""
641 if (logTZ && ((i = index(time, "-")) || (i = index(time, "+"))))
642 zone = " " substr(time, i)
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"'"
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
664 END {
665 # Print the last log.
666 if (date != "") {
667 '"$printlogline"'
668 printf "\n"
671 ' &&
674 # Exit successfully.
676 exec rm -fr $logdir
678 # Local Variables:
679 # tab-width:4
680 # End: