home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / cvs110.zip / cvs / scripts / loginfo.cmd < prev    next >
OS/2 REXX Batch file  |  1998-08-21  |  25KB  |  928 lines

  1. /*
  2. ** $Id: loginfo.cmd,v 1.1.2.4 1998/08/01 16:24:23 ahuber Exp $
  3. **
  4. ** Rexx filter to handle the log messages from the checkin of files in
  5. ** a directory. This script will group the lists of files by log message,
  6. ** and mail a single consolidated log message at the end of the commit.
  7. **
  8. ** This file assumes a pre-commit checking program that leaves the names
  9. ** of the first and last commit directories in a temporary file.
  10. **
  11. ** Commit messages are sent to the email addresses listed in the
  12. ** %CVSROOT%\maildist file on a per directory basis. This file also lists
  13. ** the names of files within %CVSROOT%\commitlogs to save commit messages
  14. ** to.
  15. **
  16. ** The file %HOME%\.cvsauthors or %ETC%\.cvsauthors contains a list of
  17. ** all known login names and their corresponding full names and email
  18. ** addresses. This information is used determine the 'From:' address for
  19. ** commit messages.
  20. **
  21. ** Usage: loginfo [-?dbS] [-i infolevel] [-a authors] repository files...
  22. **
  23. ** Options:
  24. **  -?            Display usage information.
  25. **  -i infolevel  Include RCS ID and delta info:
  26. **                 0: never,
  27. **                 1: in mail only,
  28. **                 2: in mail and logs (default).
  29. **  -d            Enable debugging.
  30. **  -b            Backup commitlogs on a monthly basis (requires gzip).
  31. **  -a authors    Specify a different path for '.cvsauthors'.
  32. **  -S            Do *not* include change summary.
  33. **
  34. ** Originally by David Hampton <hampton@cisco.com>.
  35. **
  36. ** Extensively hacked for FreeBSD by Peter Wemm <peter@dialix.com.au>,
  37. ** with parts stolen from Greg A. Woods' <woods@most.wierd.com> version.
  38. **
  39. ** Copyright (C) 1998  Andreas Huber <ahuber@ping.at>
  40. **
  41. ** This program is free software; you can redistribute it and/or
  42. ** modify it under the terms of the GNU General Public License
  43. ** as published by the Free Software Foundation; either version 2
  44. ** of the License, or (at your option) any later version.
  45. **
  46. ** This program is distributed in the hope that it will be useful,
  47. ** but WITHOUT ANY WARRANTY; without even the implied warranty of
  48. ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  49. ** GNU General Public License for more details.
  50. **
  51. ** You should have received a copy of the GNU General Public License
  52. ** along with this program; see the file COPYING. If not, write to
  53. ** the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
  54. ** Boston, MA 02111-1307, USA.
  55. */
  56. call RxFuncAdd 'SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'
  57. call SysLoadFuncs
  58.  
  59. /*
  60. ** Constants.
  61. */
  62. EXIT_SUCCESS    = 0
  63. EXIT_FAILURE    = 1
  64. EXIT_SIGNAL        = 3
  65.  
  66. FALSE            = 0
  67. TRUE            = \FALSE
  68.  
  69. TAB                = d2c(9)
  70.  
  71. STATE_NONE        = 0
  72. STATE_CHANGED    = 1
  73. STATE_ADDED        = 2
  74. STATE_REMOVED    = 3
  75. STATE_LOG        = 4
  76.  
  77. TEMP_DIR        = value('TMPDIR',, 'OS2ENVIRONMENT')
  78.  
  79. FILE_PREFIX        = '#cvs.files'
  80. LAST_FILE        = TEMP_DIR||'/'||FILE_PREFIX||'.lastdir'
  81. CHANGED_FILE    = TEMP_DIR||'/'||FILE_PREFIX||'.changed'
  82. ADDED_FILE        = TEMP_DIR||'/'||FILE_PREFIX||'.added'
  83. REMOVED_FILE    = TEMP_DIR||'/'||FILE_PREFIX||'.removed'
  84. LOG_FILE        = TEMP_DIR||'/'||FILE_PREFIX||'.log'
  85. SUMMARY_FILE    = TEMP_DIR||'/'||FILE_PREFIX||'.summary'
  86. MAIL_FILE        = TEMP_DIR||'/'||FILE_PREFIX||'.mail'
  87. SUBJ_FILE        = TEMP_DIR||'/'||FILE_PREFIX||'.subj'
  88.  
  89. CVSROOT            = get_repository()
  90.  
  91. /*
  92. ** Configurable options.
  93. **
  94. ** Where do you want the RCS ID and delta info?
  95. ** 0 = none,
  96. ** 1 = in mail only,
  97. ** 2 = RCS IDs in both mail and logs.
  98. */
  99. rcsidinfo        = 2
  100.  
  101. /*
  102. ** Debug level, 0 = off.
  103. */
  104. debug            = 0
  105.  
  106. /*
  107. ** Backup commit logs in %CVSROOT%\CVSROOT\commitlogs\ on a monthly
  108. ** basis (requires gzip).
  109. */
  110. backup            = FALSE
  111.  
  112. /*
  113. ** Look for full names and email addresses in this file. The default
  114. ** is %ETC%\.cvsauthors or %HOME%\.cvsauthors.
  115. */
  116. cvsauthors        = ''
  117.  
  118. /*
  119. ** Include a change summary with the commit message.
  120. */
  121. include_summary    = TRUE
  122.  
  123. /*
  124. ** Global names known to all procedures.
  125. */
  126. globals = 'EXIT_SUCCESS EXIT_FAILURE EXIT_SIGNAL',
  127.     'FALSE TRUE argc argv. TAB',
  128.     'STATE_NONE STATE_CHANGED STATE_ADDED STATE_REMOVED STATE_LOG',
  129.     'TEMP_DIR FILE_PREFIX LAST_FILE CHANGED_FILE ADDED_FILE',
  130.     'REMOVED_FILE LOG_FILE SUMMARY_FILE MAIL_FILE SUBJ_FILE',
  131.     'CVSROOT rcsidinfo debug backup cvsauthors id'
  132.  
  133. /*
  134. ** Main body.
  135. */
  136. main:
  137. /*
  138. ** Initialize basic variables.
  139. */
  140.     argc = 1; argv. = ''; argv.0 = 'loginfo'
  141.     signal on halt name signal_handler
  142.     do i = 1 to arg(); call setargv arg(i); end
  143.     optind = 0
  144.     options = '?id::ba::S'
  145.     do forever
  146.         c = getopt(options)
  147.         if c <= 0 then leave
  148.         select
  149.             when c = '?' | c = ':' then call usage
  150.             when c = 'i' then rcsidinfo = numeric_argument(0, 2)
  151.             when c = 'd' then debug = TRUE
  152.             when c = 'b' then backup = TRUE
  153.             when c = 'a' then cvsauthors = optarg
  154.             when c = 'S' then include_summary = FALSE
  155.             otherwise exit EXIT_FAILURE
  156.         end
  157.     end
  158.     id = SysGetPPid()
  159.     state = STATE_NONE
  160.     tag = ''
  161.     login = value('LOGNAME',, 'OS2ENVIRONMENT')
  162.     if login = '' then
  163.         login = value('USER',, 'OS2ENVIRONMENT')
  164.     path = argv.optind; optind = optind+1
  165.     files = ''
  166.     do while optind < argc
  167.         files = concat(files, argv.optind, ' ')
  168.         optind = optind+1
  169.     end
  170.     parse var path modulename '/' dir
  171.     if dir = '' then dir = '.'
  172.     dir = dir||'/'
  173.  
  174.     call append_line MAIL_FILE||'.'||id, read_maildist(path)
  175.     call append_line SUBJ_FILE||'.'||id, path files
  176. /*
  177. ** Check for a new directory first. This will always appear as a
  178. ** single item in the argument list, and an empty log message.
  179. */
  180.     if pos('- New directory', files) = 1 then do
  181.         header = build_header()
  182.         text. = ''; text.0 = 0
  183.         call push header
  184.         call push ''
  185.         call push '  '||path files
  186.         call do_changes_file
  187.         call mail_notification login
  188.         call cleanup_tmpfiles
  189.         exit EXIT_SUCCESS
  190.     end
  191. /*
  192. ** Check for an import command. This will always appear as a
  193. ** single item in the argument list, and a log message.
  194. */
  195.     if pos('- Imported sources', files) = 1 then do
  196.         header = build_header()
  197.         text. = ''; text.0 = 0
  198.         call push header
  199.         call push ''
  200.         call push '  '||path files
  201.         call push ''
  202.         call push '  Log:'
  203.         i = text.0
  204.         do while lines() > 0
  205.             call push '  '||linein()
  206.         end
  207.         call compress_log i
  208.         call do_changes_file
  209.         call mail_notification login
  210.         call cleanup_tmpfiles
  211.         exit EXIT_SUCCESS
  212.     end
  213. /*
  214. ** Iterate over the body of the message collecting information.
  215. */
  216.     tag = 'HEAD'
  217.     call init_state STATE_CHANGED
  218.     call init_state STATE_ADDED
  219.     call init_state STATE_REMOVED
  220.     text. = ''; text.0 = 0
  221.     do while lines() > 0
  222.         line = strip(translate(linein(), ' ', TAB), 't')
  223.         if pos('Revision/Branch:', line) = 1 then do
  224.             parse var line . ':' tag
  225.             iterate
  226.         end
  227.         if word(line, 1) = 'Tag:' then do
  228.             parse var line . ': ' tag
  229.             iterate
  230.         end
  231.         if strip(line, 'l') = 'No tag' then do
  232.             tag = 'HEAD'
  233.             iterate
  234.         end
  235.         if pos('Modified Files', line) = 1 then do
  236.             state = STATE_CHANGED
  237.             iterate
  238.         end
  239.         if pos('Added Files', line) = 1 then do
  240.             state = STATE_ADDED
  241.             iterate
  242.         end
  243.         if pos('Removed Files', line) = 1 then do
  244.             state = STATE_REMOVED
  245.             iterate
  246.         end
  247.         if pos('Log Message', line) = 1 then do
  248.             state = STATE_LOG
  249.             iterate
  250.         end
  251.         if state = STATE_LOG then do
  252.             temp = translate(line)
  253.             if temp = 'PR:' |,
  254.                 temp = 'REVIEWED BY:' |,
  255.                 temp = 'SUBMITTED BY:' |,
  256.                 temp = 'OBTAINED FROM:' then iterate
  257.             call push line
  258.         end
  259.         else if state \= STATE_NONE then
  260.             call push_tag state, tag, line
  261.     end
  262. /*
  263. ** Skip leading and trailing blank lines from the log message. Also
  264. ** compress multiple blank lines in the body of the message down to a
  265. ** single blank line.
  266. ** (Note, this only does the mail and changes log, not the rcs log),
  267. */
  268.     call compress_log
  269. /*
  270. ** Find the log that matches this log message.
  271. */
  272.     do i = 0
  273.         if \exists(LOG_FILE||'.'||i||'.'||id) then leave
  274.         call read_logfile LOG_FILE||'.'||i||'.'||id, ''
  275.         if lines.0 = 0 then leave
  276.         if compare_log() then leave
  277.     end
  278. /*
  279. ** Spit out the information gathered in this pass.
  280. */
  281.     filename = ADDED_FILE||'.'||i||'.'||id
  282.     call append_state_to_file filename, dir, STATE_ADDED
  283.  
  284.     filename = CHANGED_FILE||'.'||i||'.'||id
  285.     call append_state_to_file filename, dir, STATE_CHANGED
  286.  
  287.     filename = REMOVED_FILE||'.'||i||'.'||id
  288.     call append_state_to_file filename, dir, STATE_REMOVED
  289.  
  290.     call write_logfile LOG_FILE||'.'||i||'.'||id
  291.  
  292.     if rcsidinfo \= 0 & include_summary then do
  293.         filename = SUMMARY_FILE||'.'||i||'.'||id
  294.         tags = states.STATE_ADDED.keys
  295.         do i = 1 to words(tags)
  296.             call change_summary_added filename, word(tags, i)
  297.         end
  298.         tags = states.STATE_CHANGED.keys
  299.         do i = 1 to words(tags)
  300.             call change_summary_changed filename, word(tags, i)
  301.         end
  302.         tags = states.STATE_REMOVED.keys
  303.         do i = 1 to words(tags)
  304.             call change_summary_removed filename, word(tags, i)
  305.         end
  306.     end
  307. /*
  308. ** Check wether this is the last directory. If not, quit.
  309. */
  310.     if exists(LAST_FILE||'.'||id) then do
  311.         line = read_line(LAST_FILE||'.'||id)
  312.         if right(line, length(path)) \= path then do
  313.             say 'More commits to come...'
  314.             exit 0
  315.         end
  316.     end
  317. /*
  318. ** This is it. The commits are all finished. Lump everthing together
  319. ** into a single message, fire a copy off to the mailing list, and drop
  320. ** it on the end of the Changes file.
  321. */
  322.     header = build_header()
  323. /*
  324. ** Produce the final compilation of the log messages.
  325. */
  326.     text. = ''; text.0 = 0
  327.     call push header
  328.     call push ''
  329.     do i = 0
  330.         if \exists(LOG_FILE||'.'||i||'.'||id) then leave
  331.         call read_logfile CHANGED_FILE||'.'||i||'.'||id, ''
  332.         if lines.0 > 0 then
  333.             call format_lists 'Modified'
  334.         call read_logfile ADDED_FILE||'.'||i||'.'||id, ''
  335.         if lines.0 > 0 then
  336.             call format_lists 'Added'
  337.         call read_logfile REMOVED_FILE||'.'||i||'.'||id, ''
  338.         if lines.0 > 0 then
  339.             call format_lists 'Removed'
  340.         call read_logfile LOG_FILE||'.'||i||'.'||id, '  '
  341.         if lines.0 > 0 then do
  342.             call push '  Log:'
  343.             call push_lines
  344.         end
  345.         if rcsidinfo = 2 & exists(SUMMARY_FILE||'.'||i||'.'||id) then do
  346.             call push '  '
  347.             call push '  Revision  Changes    Path'
  348.             call read_logfile SUMMARY_FILE||'.'||i||'.'||id, '  '
  349.             call push_lines
  350.         end
  351.         call push ''
  352.     end
  353. /*
  354. ** Put the log message at the beginning of the Changes file.
  355. */
  356.     call do_changes_file
  357. /*
  358. ** Now generate the extra info for the mail message.
  359. */
  360.     if rcsidinfo = 1 then do
  361.         revhdr = 0
  362.         do i = 0
  363.             if \exists(LOG_FILE||'.'||i||'.'||id) then leave
  364.             if exists(SUMMARY_FILE||'.'||i||'.'||id) then do
  365.                 if revhdr = 0 then
  366.                     call push 'Revision  Changes    Path'
  367.                 call read_logfile SUMMARY_FILE||'.'||i||'.'||id, ''
  368.                 call push_lines
  369.                 revhdr = revhdr+1
  370.             end
  371.         end
  372.         if revhdr > 0 then
  373.             call push ''
  374.     end
  375. /*
  376. ** Mail out the notification.
  377. */
  378.     call mail_notification login
  379.     call cleanup_tmpfiles
  380.     exit EXIT_SUCCESS
  381. /*
  382. ** Subroutines.
  383. */
  384. die: procedure expose (globals)
  385.     parse arg text
  386.     call lineout 'stderr:', argv.0||': '||text
  387.     exit EXIT_FAILURE
  388.  
  389. push: procedure expose text.
  390.     parse arg line
  391.     i = text.0+1
  392.     text.i = line
  393.     text.0 = i
  394.     return
  395.  
  396. /*
  397. ** Mail distribution and commit log mapping.
  398. */
  399. read_maildist: procedure expose (globals)
  400.     parse arg dir
  401.     filename = CVSROOT||'/CVSROOT/maildist'
  402.     if stream(filename, 'c', 'open read') \= 'READY:' then
  403.         return ''
  404.     default_dist = ''; all_dist = ''; mail_dist = ''
  405.     do while lines(filename) > 0
  406.         line = space(translate(linein(filename), '  ', ','||TAB))
  407.         if left(line, 1) = '#' then iterate
  408.         parse var line prefix dist
  409.         if prefix = '' | dist = '' then iterate
  410.         if prefix = 'DEFAULT' then
  411.             default_dist = concat(default_dist, dist, ' ')
  412.         else if prefix = 'ALL' then
  413.             all_dist = concat(all_dist, dist, ' ')
  414.         else if is_prefix(prefix, dir) then
  415.             mail_dist = concat(mail_dist, dist, ' ')
  416.     end
  417.     call stream filename, 'c', 'close'
  418.     if mail_dist = '' then
  419.         mail_dist = concat(all_dist, default_dist, ' ')
  420.     else
  421.         mail_dist = concat(all_dist, mail_dist, ' ')
  422.     return mail_dist
  423.  
  424. concat: procedure
  425.     parse arg s1, s2, sep
  426.     if s1 = '' then return s2
  427.     if s2 = '' then return s1
  428.     return s1||sep||s2
  429.  
  430. is_prefix: procedure expose (globals)
  431.     parse arg prefix, dir
  432.     if prefix = dir then return TRUE
  433.     if length(prefix) >= length(dir) then return FALSE
  434.     parse var dir (prefix) '/' dir
  435.     return dir \= ''
  436.  
  437. append_line: procedure expose (globals)
  438.     parse arg filename, line
  439.     if stream(filename, 'c', 'open write') \= 'READY:' then
  440.         call die 'Cannot open for append file '||filename||'.'
  441.     call lineout filename, line
  442.     call stream filename, 'c', 'close'
  443.     return
  444.  
  445. build_header: procedure expose login
  446.     date = insert('/', insert('/', date('s'), 4), 7)
  447.     time = time('n')
  448.     return left(login, 8)||'    '||date||' '||time
  449.  
  450. do_changes_file: procedure expose (globals) text.
  451.     parse arg
  452.     call read_logfile MAIL_FILE||'.'||id, ''
  453.     unique. = FALSE
  454.     do i = 1 to lines.0
  455.         do j = 1 to words(lines.i)
  456.             category = word(lines.i, j)
  457.             if pos('@', category) > 0 then iterate
  458.             if unique.category then iterate
  459.             unique.category = TRUE
  460.             changes = CVSROOT||'/CVSROOT/commitlogs/'||category
  461.             if backup then
  462.                 call backup_changes changes
  463.             call stream changes, 'c', 'open write'
  464.             do k = 1 to text.0
  465.                 call lineout changes, text.k
  466.             end
  467.             call lineout changes, ''
  468.             call stream changes, 'c', 'close'
  469.         end
  470.     end
  471.     return
  472.  
  473. backup_changes: procedure expose (globals)
  474.     parse arg filename
  475.     parse value stream(filename, 'c', 'query datetime') with,
  476.         m '-' . '-' y ' '
  477.     if m \= '' & y \= '' then if m \= substr(date('s'), 5, 2) then do
  478.         if y >= 70 then y = 1900+y; else y = 2000+y
  479.         filename = translate(filename, '\', '/')
  480.         '@rename' filename filename||'.'||y||'-'||m '>nul'
  481.         if rc = 0 then
  482.             '@gzip -q' filename||'.'||y||'-'||m '>nul'
  483.         if rc \= 0 then
  484.             call lineout 'stderr:', argv.0||': Warning:',
  485.                 'Couldn''t backup "'||filename||'".'
  486.     end
  487.     return
  488.  
  489. read_logfile: procedure expose lines.
  490.     parse arg filename, leader
  491.     lines. = ''; lines.0 = 0
  492.     call stream filename, 'c', 'open read'
  493.     i = 0
  494.     do while lines(filename) > 0
  495.         i = i+1
  496.         lines.i = leader||linein(filename)
  497.     end
  498.     lines.0 = i
  499.     call stream filename, 'c', 'close'
  500.     return
  501.  
  502. cleanup_tmpfiles: procedure expose (globals)
  503.     filemask = TEMP_DIR||'/'||FILE_PREFIX||'.*.'||id
  504.     call SysFileTree translate(filemask, '\', '/'),,
  505.         'files', 'FO', '*----'
  506.     do i = 1 to files.0
  507.         call SysFileDelete files.i
  508.     end
  509.     return
  510.  
  511. init_state: procedure expose states.
  512.     parse arg state
  513.     call value 'states.'||state||'.keys', ''
  514.     return
  515.  
  516. init_tag: procedure expose states.
  517.     parse arg state, tag
  518.     call value 'states.'||state||'.'||tag||'.', ''
  519.     call value 'states.'||state||'.'||tag||'.0', 0
  520.     return
  521.  
  522. push_tag: procedure expose states.
  523.     parse arg state, tag, line
  524.     tags = value('states.'||state||'.keys')
  525.     if wordpos(tag, tags) = 0 then do
  526.         tags = tags||' '||tag
  527.         call init_tag state, tag
  528.         call value 'states.'||state||'.keys', tags
  529.     end
  530.     n = value('states.'||state||'.'||tag||'.0')
  531.     do i = 1 to words(line)
  532.         n = n+1
  533.         call value 'states.'||state||'.'||tag||'.'||n, word(line, i)
  534.     end
  535.     call value 'states.'||state||'.'||tag||'.0', n
  536.     return
  537.  
  538. exists: procedure
  539.     parse arg filename
  540.     return stream(filename, 'c', 'query size') \= ''
  541.  
  542. compare_log: procedure expose text. lines.
  543.     if text.0 \= lines.0 then return 0
  544.     do i = 1 to text.0
  545.         if text.i \= lines.i then return 0
  546.     end
  547.     return 1
  548.  
  549. compress_log: procedure expose text.
  550.     offset = 0
  551.     if arg(1, 'e') then parse arg offset
  552.     i = text.0
  553.     do while i > offset & text.i = ''; i = i-1; end
  554.     text.0 = i
  555.     i = offset+1
  556.     if left(text.i, 1) = '{' then do
  557.         p = verify(text.i, ' }'||TAB, 'm')
  558.         if substr(text.i, p, 1) = '}' then do
  559.             text.i = substr(text.i,,
  560.                 p+verify(substr(text.i, p+1), ' '||TAB))
  561.         end
  562.     end
  563.     do while i <= text.0 & text.i = ''; i = i+1; end
  564.     j = offset
  565.     do while i <= text.0
  566.         j = j+1
  567.         text.j = text.i
  568.         if text.i \= '' then do; i = i+1; iterate; end
  569.         i = i+1
  570.         do while i <= text.0 & text.i = ''; i = i+1; end
  571.     end
  572.     text.0 = j
  573.     return
  574.  
  575. append_state_to_file: procedure expose (globals) states.
  576.     parse arg filename, dir, state
  577.     tags = value('states.'||state||'.keys')
  578.     do i = 1 to words(tags)
  579.         call append_names_to_file filename, dir, word(tags, i), state
  580.     end
  581.     return
  582.  
  583. append_names_to_file: procedure expose (globals) states.
  584.     parse arg filename, dir, tag, state
  585.     n = value('states.'||state||'.'||tag||'.0')
  586.     if n > 0 then do
  587.         if stream(filename, 'c', 'open write') \= 'READY:' then
  588.             call die 'Cannot open for append file '||filename||'.'
  589.         call lineout filename, dir
  590.         call lineout filename, tag
  591.         do i = 1 to n
  592.             call lineout filename,,
  593.                 value('states.'||state||'.'||tag||'.'||i)
  594.         end
  595.         call stream filename, 'c', 'close'
  596.     end
  597.     return
  598.  
  599. write_logfile: procedure expose (globals) text.
  600.     parse arg filename
  601.     call SysFileDelete translate(filename, '\', '/')
  602.     if stream(filename, 'c', 'open write') \= 'READY:' then
  603.         call die 'Cannot open for write log file '||filename||'.'
  604.     do i = 1 to text.0
  605.         call lineout filename, text.i
  606.     end
  607.     call stream filename, 'c', 'close'
  608.     return
  609.  
  610. /*
  611. ** Write these one day.
  612. */
  613. change_summary_removed: procedure expose (globals) states.
  614.     parse arg filename, tag
  615.     return
  616.  
  617. change_summary_added: procedure expose (globals) states.
  618.     parse arg filename, tag
  619.     return
  620.  
  621. /*
  622. ** Do an 'cvs -Qn status' on each file in the arguments,
  623. ** and extract info.
  624. */
  625. change_summary_changed: procedure expose (globals) states.
  626.     parse arg filename, tag
  627.     queue = rxqueue('create')
  628.     call rxqueue 'set', queue
  629.     do i = 1 to value('states.STATE_CHANGED.'||tag||'.0')
  630.         file = value('states.STATE_CHANGED.'||tag||'.'||i)
  631.         if file = '' then iterate
  632.         'cvs -Qn status '||file||' | rxqueue '||queue
  633.         rev = ''
  634.         delta = ''
  635.         rcsfile = ''
  636.         do while queued() > 0
  637.             line = space(translate(linein('queue:'), ' ', TAB))
  638.             if pos('Repository revision:', line) = 1 then
  639.                 parse var line . ': ' rev ' ',
  640.                     (CVSROOT) '/' rcsfile ',v'
  641.         end
  642.         if rev \= '' & rcsfile \= '' then do
  643.             'cvs -Qn log -N -r'||rev||' '||file||' | rxqueue '||queue
  644.             do while queued() > 0
  645.                 line = linein('queue:')
  646.                 if pos('date:', line) = 1 then
  647.                     parse var line . 'lines:' delta
  648.             end
  649.         end
  650.         call append_line filename, left(rev, 9)||left(delta, 12)||rcsfile
  651.     end
  652.     call rxqueue 'delete', queue
  653.     return
  654.  
  655. read_line: procedure expose (globals)
  656.     parse arg filename
  657.     if stream(filename, 'c', 'open read') \= 'READY:' then
  658.         call die 'Cannot open for read file '||filename||'.'
  659.     line = linein(filename)
  660.     call stream filename, 'c', 'close'
  661.     return line
  662.  
  663. push_lines: procedure expose text. lines.
  664.     do i = 1 to lines.0
  665.         call push lines.i
  666.     end
  667.     return
  668.  
  669. format_lists: procedure expose lines. text.
  670.     parse arg header
  671.     files. = ''; files.0 = 0
  672.     lastdir = ''
  673.     lastsep = ''
  674.     do i = 1 to lines.0
  675.         if right(lines.i, 1) = '/' then do
  676.             if lastdir \= '' then
  677.                   call format_names lastdir
  678.             lastdir = lines.i
  679.             tag = ''
  680.             files. = ''; files.0 = 0
  681.         end
  682.         else if tag = '' then do
  683.             tag = lines.i
  684.             if header||tag = lastsep then iterate
  685.             lastsep = header||tag
  686.             if tag = 'HEAD' then
  687.                 call push '  '||header||' files:'
  688.             else
  689.                 call push '  '||left(header||' files:', 22)||,
  690.                     ' (Branch: '||tag||')'
  691.         end
  692.         else do
  693.             n = files.0+1
  694.             files.n = lines.i
  695.             files.0 = n
  696.         end
  697.     end
  698.     call format_names lastdir
  699.     return
  700.  
  701. format_names: procedure expose files. text.
  702.     parse arg dir
  703.     indent = length(dir)
  704.     if indent < 20 then
  705.         indent = 20
  706.     line = '    '||left(dir, indent)
  707.     do i = 1 to files.0
  708.         if length(line)+length(files.i) > 66 then do
  709.             call push line
  710.             line = '    '||left('', indent)
  711.         end
  712.         line = line||' '||files.i
  713.     end
  714.     call push line
  715.     return
  716.  
  717. mail_notification: procedure expose (globals) text.
  718.     parse arg login
  719.     from = read_author(login)
  720.     call read_logfile MAIL_FILE||'.'||id, ''
  721.     to = ''
  722.     unique. = FALSE
  723.     do i = 1 to lines.0
  724.         do j = 1 to words(lines.i)
  725.             word = word(lines.i, j)
  726.             if pos('@', word) = 0 then iterate
  727.             if unique.word then iterate
  728.             unique.word = TRUE
  729.             to = concat(to, word, ' ')
  730.         end
  731.     end
  732.     if to = '' then return
  733.     say 'Mailing the commit message...'
  734.     filename = translate(TEMP_DIR||'/cvs-mail.?????', '\', '/')
  735.     filename = SysTempFileName(filename)
  736.     call stream filename, 'c', 'open write'
  737.     call lineout filename, 'From:' from
  738.     call lineout filename, 'To:' word(to, 1)
  739.     if words(to) > 1 then
  740.         call lineout filename, 'Bcc:' space(subword(to, 2),, ',')
  741.     subject = 'Subject: cvs commit:'
  742.     call read_logfile SUBJ_FILE||'.'||id, ''
  743.     subjlines = 0; subjwords = 0
  744.     do i = 1 to lines.0
  745.         line = lines.i
  746.         do j = 1 to words(line)
  747.             if subjwords > 2 &,
  748.                     length(subject||' '||word(line, j)) > 75 then do
  749.                 if subjlines > 2 then
  750.                     subject = subject||' ...'
  751.                 call lineout filename, subject
  752.                 if subjlines > 2 then do
  753.                     subject = ''
  754.                     leave i
  755.                 end
  756.                 subject = '        '
  757.                 subjwords = 0
  758.                 subjlines = subjlines+1
  759.             end
  760.             subject = subject||' '||word(line, j)
  761.             subjwords = subjwords+1
  762.         end
  763.     end
  764.     if subject \= '' then
  765.         call lineout filename, subject
  766.     call lineout filename, ''
  767.     do i = 1 to text.0
  768.         call lineout filename, text.i
  769.     end
  770.     call stream filename, 'c', 'close'
  771.     '@sendmail -odb -oem -t -a' filename
  772.     call SysFileDelete translate(filename, '\', '/')
  773.     return
  774.  
  775. find_authors: procedure expose (globals)
  776.     parse arg env
  777.     dir = value(env,, 'OS2ENVIRONMENT')
  778.     if dir \= '' then if exists(dir||'/.cvsauthors') then
  779.         return dir||'/.cvsauthors'
  780.     return ''
  781.  
  782. read_author: procedure expose (globals)
  783.     parse arg login
  784.     filename = cvsauthors
  785.     if filename = '' then do
  786.         filename = find_authors('HOME')
  787.         if filename = '' then
  788.             filename = find_authors('ETC')
  789.     end
  790.     mailaddr = ''
  791.     if filename \= '' then do
  792.         if stream(filename, 'c', 'open read') \= 'READY:' then
  793.             call die 'Cannot open' filename 'for reading.'
  794.         do while lines(filename) > 0
  795.             line = linein(filename)
  796.             parse var line (login) '|' fullname '|' mailaddr
  797.             if mailaddr \= '' then leave
  798.         end
  799.         call stream filename, 'c', 'close'
  800.     end
  801.     if mailaddr \= '' then do
  802.         if fullname = '' then
  803.             from = mailaddr
  804.         else
  805.             from = '"'||fullname||'" <'||mailaddr||'>'
  806.     end
  807.     else
  808.         from = login||'@'||get_hostname()
  809.     return from
  810.  
  811. get_hostname: procedure expose (globals)
  812.     hostname = value('HOSTNAME',, 'OS2ENVIRONMENT')
  813.     if hostname \= '' then return hostname
  814.     queue = rxqueue('create')
  815.     call rxqueue 'set', queue
  816.     'hostname | rxqueue '||queue
  817.     if rc = 0 then if lines('queue:') > 0 then
  818.         hostname = linein('queue:')
  819.     call rxqueue 'delete', queue
  820.     if hostname = '' then
  821.         hostname = 'localhost'
  822.     return hostname
  823.  
  824. get_repository: procedure
  825.     cvsroot = value('CVSROOT',, 'OS2ENVIRONMENT')
  826.     if left(cvsroot, 1) = ':' then
  827.         parse var cvsroot ':' method ':' cvsroot
  828.     else if pos(cvsroot, ':') = 0 then
  829.         method = 'local'
  830.     else
  831.         method = 'server'
  832.     if method \= 'local' then
  833.         parse var cvsroot . ':' cvsroot
  834.     return cvsroot
  835.  
  836. usage: procedure expose (globals)
  837.     say 'Usage: '||argv.0||' [-?dbS] [-i infolevel] [-a authors]',
  838.         'repository files...'
  839.     exit EXIT_USAGE
  840.  
  841. numeric_argument: procedure expose (globals) optopt optarg
  842.     parse arg minval, maxval
  843.     if \datatype(optarg, 'w') then
  844.         call die '-'||optopt optarg||': invalid argument.'
  845.     if optarg < minval | optarg > maxval then
  846.         call die '-'||optopt optarg||': argument out of range',
  847.             '['||minval||', '||maxval||'].'
  848.     return optarg
  849.  
  850. getopt: procedure expose (globals) optind optarg optopt optptr
  851.     parse arg options
  852.     if optind = 0 then optptr = 0
  853.     if optptr = 0 | optptr > length(argv.optind) then do
  854.         if optind >= argc then return -1
  855.         optind = optind+1
  856.         optptr = 1
  857.         if substr(argv.optind, optptr, 1) \= '-' then return 0
  858.         optptr = optptr+1
  859.     end
  860.     optopt = substr(argv.optind, optptr, 1)
  861.     optptr = optptr+1
  862.     if optopt = '-' then do
  863.         optind = optind+1
  864.         optptr = 0
  865.         return -1
  866.     end
  867.     i = pos(optopt, options)
  868.     if optopt = ':' | i = 0 then do
  869.         say argv.0||': -'||optopt||' is not a valid option.'
  870.         return '?'
  871.     end
  872.     if substr(options, i+1, 1) = ':' then do
  873.         if optptr <= length(argv.optind) then do
  874.             optarg = substr(argv.optind, optptr)
  875.             optptr = 0
  876.             return optopt;
  877.         end
  878.         if substr(options, i+2, 1) = ':' then do
  879.             i = optind+1
  880.             optptr = 1
  881.             if i < argc & substr(argv.i, optptr, 1) \= '-' then do
  882.                 optind = i
  883.                 optarg = argv.optind
  884.                 optptr = 0
  885.                 return optopt
  886.             end
  887.             say argv.0||': -'||optopt||' is missing an argument.'
  888.             return ':'
  889.         end
  890.         optptr = 0
  891.     end
  892.     optarg = ''
  893.     return optopt
  894.  
  895. setargv: procedure expose (globals)
  896.     parse arg args
  897.     inquote = FALSE
  898.     do forever
  899.         parse var args arg args
  900.         if arg = '' then leave
  901.         quotes = FALSE
  902.         i = 1
  903.         do forever
  904.             i = pos('"', arg, i)
  905.             if i = 0 then leave
  906.             if i > 1 then if substr(arg, i-1, 1) = '\' then do
  907.                 arg = delstr(arg, i-1, 1)
  908.                 iterate
  909.             end
  910.             arg = delstr(arg, i, 1)
  911.             quotes = \quotes
  912.         end
  913.         if inquote then
  914.             argv.argc = argv.argc arg
  915.         else do
  916.             argv.argc = arg
  917.             argc = argc+1
  918.         end
  919.         if quotes then inquote = \inquote
  920.     end
  921.     return
  922.  
  923. signal_handler:
  924.     call lineout 'stderr:', argv.0||': terminated by SIGINT.'
  925.     call cleanup_tmpfiles
  926.     exit EXIT_SIGNAL
  927.  
  928.