/*
** $Id: cvs2log.cmd,v 1.1 1999/02/10 23:17:52 tanchs1 Exp $
**
** CVS to ChangeLog generator.
**
** Generate a change log prefix from a CVS repository and the
** ChangeLog (if any). The new prefix is prepended to the ChangeLog.
** The editor specified by the CVSEDITOR environment variable (or a
** default editor, if CVSEDITOR is not set) is invoked with the new
** ChangeLog as an argument.
**
** The files %HOME%\.cvsauthors and/or %ETC%\.cvsauthors contain a
** list of all known login names and their corresponding full names
** and email addresses.
**
** Usage: cvs2log [-?Rgv] [-c changelog] [-d date] [-i indent]
**   [-l length] [-t tabwidth] [-A authors] [files...]
**
** Options:
**  -?            Display usage information.
**  -R            Process directories recursively.
**  -c changelog  Specify a different name for the ChangeLog
**                (default 'ChangeLog').
**  -g            Use a 'global' changelog, as opposed to a ChangeLog
**                local to each directory.
**  -d date       Specify a date argument to 'cvs log'.
**  -i indent     Indent ChangeLog lines by 'indent' spaces (default 8).
**  -l length     Try to limit log lines to 'length' characters
**                (default 79).
**  -t tabwidth   Tab stops are every 'tabwidth' characters (default 8).
**  -v            Append RCS revision to file names in log lines.
**  -a authors    Specify a different path for '.cvsauthors'.
**
** 'files...' can be any combination of files (including wildcards) and
** (CVS controlled) directories.
**
** Log entries that start with '#' are ignored.
** Log entries that start with '{topic}', where 'topic' contains
** neither white space nor '}', are clumped together.
**
** Based on rcs2log.sh by Paul Eggert <eggert@twinsun.com>.
**
** Copyright (C) 1998  Andreas Huber <ahuber@ping.at>
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the GNU General Public License
** as published by the Free Software Foundation; either version 2
** of the License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; see the file COPYING. If not, write to
** the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
** Boston, MA 02111-1307, USA.
*/
call RxFuncAdd 'SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'
call SysLoadFuncs

/*
** Constants.
**/
EXIT_SUCCESS		= 0
EXIT_FAILURE		= 1
EXIT_USAGE			= 2
EXIT_SIGNAL			= 3

FALSE				= 0
TRUE				= \FALSE

MONTHS				= 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'

SOH					= d2c(1)
TAB					= d2c(9)

CVSEDITOR_DEFAULT	= 'tedit'

/*
** Configurable options.
*/
recursive			= FALSE
datearg				= ''
global_changelog	= FALSE
changelog_name		= 'ChangeLog'
changelog_prefix	= 'ChangeLog.new'
line_length			= 79
line_indent			= 8
tabwidth			= 8
cvsauthors			= ''
revision			= FALSE
cvseditor			= ''

/*
** Global names known to all procedures.
*/
globals = 'EXIT_SUCCESS EXIT_USAGE EXIT_FAILURE EXIT_SIGNAL',
	' FALSE TRUE argc argv. MONTHS SOH TAB',
	'recursive datearg global_changelog changelog_name changelog_prefix',
	'line_length line_indent tabwidth cvsauthors revision cvseditor',
	'fullname. mailaddr.'

/*
** Main body.
*/
main:
	argc = 1; argv. = ''; argv.0 = 'cvs2log'
	signal on halt name signal_handler
	do i = 1 to arg(); call setargv arg(i); end
	optind = 0
	options = '?Rc::d::gi::l::t::va::'
	do forever
		c = getopt(options)
		if c <= 0 then leave
		select
			when c = '?' | c = ':' then call usage
			when c = 'R' then recursive = TRUE
			when c = 'c' then changelog_name = optarg
			when c = 'g' then global_changelog = TRUE
			when c = 'd' then datearg = ' "-d'||optarg||'"'
			when c = 'i' then line_indent = numeric_argument(1, 100)
			when c = 'l' then line_length = numeric_argument(40, 200)
			when c = 't' then tabwidth = numeric_argument(0, 100)
			when c = 'v' then revision = TRUE
			when c = 'a' then cvsauthors = optarg
			otherwise exit EXIT_FAILURE
		end
	end
	files = ''
	do while optind < argc
		path = translate(argv.optind, '/', '\')
		i = lastpos('/', path)
		if i < length(path) then if is_directory(path) then do
			path = path||'/'
			i = length(path)
		end
		dir = substr(path, 1, i)
		if dir = '' then
			dir = './'
		filename = substr(argv.optind, i+1)
		if filename = '' then
			filename = '*'
		if wordpos(dir, files) = 0 then do
			files = files dir
			files.dir = ''
		end
		if filename = '*' then
			files.dir = filename
		else if files.dir \= '*' then
			files.dir = files.dir filename
		optind = optind+1
	end
	cvseditor = value('CVSEDITOR',, 'OS2ENVIRONMENT')
	if cvseditor = '' then cvseditor = CVSEDITOR_DEFAULT
	call read_authors
	repository = ''
	if global_changelog then do
		call parse_changelog './'
		repository = get_repository('./')
		text. = ''; text.0 = 0
	end
	if files = '' then
		call do_directory './', '*', repository
	else do
		do i = 1 to words(files)
			dir = word(files, i)
			call do_directory dir, files.dir, repository
		end
	end
	if global_changelog then do
		call sort_log
		call format_log './'
		call edit_changelog './'
	end
	exit EXIT_SUCCESS

edit_changelog: procedure expose (globals)
	parse arg dir
	dir = translate(dir, '\', '/')
	if exists(dir||changelog_prefix) then do
		tempname = SysTempFileName(dir||changelog_name||'.?????')
		'@copy' dir||changelog_prefix '+' dir||changelog_name,
			tempname '>nul 2>&1'
		'@erase' dir||changelog_name dir||changelog_prefix '>nul 2>&1'
		'@rename' tempname dir||changelog_name  '>nul 2>&1'
		'@'||cvseditor dir||changelog_name
	end
	return

parse_timestamp: procedure expose (globals)
	parse arg line
	if verify(line, ' '||TAB) > 1 then return ''
	parse var line year '-' month '-' day .
	if year \= '' & month \= '' & day \= '' then
		return ' "-d>'||year||'-'||month||'-'||day||'"'
	parse var line . ' ' month ' ' day ' ' . ':' . ':' . ' ' year .
	if year \= '' & month \= '' & day \= '' then do
		month = wordpos(month, MONTHS)
		return ' "-d>'||year||'-'||month||'-'||day||'"'
	end
	parse var line day ' ' month ' ' year .
	if year \= '' & month \= '' & day \= '' then do
		month = wordpos(month, MONTHS)
		return ' "-d>'||year||'-'||month||'-'||day||'"'
	end
	parse var line . ', ' day ' ' month ' ' year .
	if year \= '' & month \= '' & day \= '' then do
		month = wordpos(month, MONTHS)
		return ' "-d>'||year||'-'||month||'-'||day||'"'
	end
	return ''

parse_changelog: procedure expose (globals) tz dateval
	parse arg dir
	dateval = datearg
	filename = dir||changelog_name
	if stream(filename, 'c', 'query size') = 0 then
		return FALSE
	call stream filename, 'c', 'open read'
	do while lines(filename) > 0
		line = translate(linein(filename), ' ', TAB)
		if dateval = '' then dateval = parse_timestamp(line)
		line = space(line, 0)
		parse var line '#change-log-time-zone-rule:' tz
		if tz \= '' then leave
	end
	call stream filename, 'c', 'close'
	if tz = '' then
		tz = value('TZ',, 'OS2ENVIRONMENT')
	return TRUE

exists: procedure expose (globals)
	parse arg filename
	return stream(filename, 'c', 'query size') \= ''

is_directory: procedure expose (globals)
	parse arg path
	if pos('?', path) > 0 | pos('*', path) > 0 then
		return false
	filespec = translate(path, '\', '/')
	if SysFileTree(filespec, filelist, 'do', '*+-*-') \= 0 then
		call die '"'||path||'": Not enough memory'
	return filelist.0 > 0

do_directory: procedure expose (globals) dateval tz text.
	parse arg dir, filelist, repository
	call do_files dir, filelist, repository
	if filelist = '*' & recursive then do
		call read_entries dir
		do i = 1 to entries.0
			dir = dir||entries.i||'/'
			call do_directory dir, '*', repository
		end
	end
	return

do_files: procedure expose (globals) dateval tz text.
	parse arg dir, filelist, repository
	if \global_changelog then do
		call parse_changelog dir
		repository = get_repository(dir)
		text. = ''; text.0 = 0
	end
	call parse_log dir, filelist, repository
	if \global_changelog then do
		call sort_log
		call format_log dir
		call edit_changelog dir
	end
	return

format_log: procedure expose (globals) tz text.
	parse arg dir
	changelog = dir||changelog_prefix
	if stream(changelog, 'c', 'open write') \= 'READY:' then
		call die 'Cannot write to' changelog '.'
	indent_string = ''
	i = line_indent
	if tabwidth > 0 then do while i >= tabwidth
		indent_string = indent_string||TAB
		i = i-tabwidth
	end
	do i; indent_string = indent_string||' '; end
	hostname = get_hostname()
	date = ''; time = ''; author = ''; log = ''
	clumpname = ''; files = ''; filesknown. = FALSE
	do i = 1 to text.0
		parse var text.i nfilename nrev ndate ntime nauthor (SOH) nlog
		nclumpname = ''
		if left(nlog, 1) = '{' then do
			p = verify(nlog, ' }'||TAB||SOH, 'm')
			if substr(nlog, p, 1) = '}' then do
				nclumpname = substr(nlog, 2, p-2)
				nlog = substr(nlog,,
					p+verify(substr(nlog, p+1), ' '||TAB))
			end
		end
		if log \= nlog | date \= ndate | author \= nauthor then do
			if date \= '' then do
				call print_log changelog, files, log
				if nclumpname = '' | nclumpname \= clumpname then
					call lineout changelog, ''
			end
			clumpname = nclumpname
			log = nlog
			files = ''; filesknown. = FALSE
		end
		if date \= ndate | author \= nauthor then do
			date = ndate; time = ntime; author = nauthor
			zone = ''
			if tz \= '' then do
				p = pos('-', time)
				if p = 0 then p = pos('+', time)
				if p > 0 then zone = substr(time, p)
			end
			if fullname.author \= '' then
				fullname = fullname.author
			else
				fullname = author
			call charout changelog, date||zone||'  '||fullname
			if mailaddr.author \= '' then
				mailaddr = mailaddr.author
			else
				mailaddr = author||'@'||hostname
			call lineout changelog, '  <'||mailaddr||'>'
			call lineout changelog, ''
		end
		if \filesknown.nfilename then do
			filesknown.nfilename = TRUE
			if files = '' then
				files = ' '||nfilename
			else
				files = files||', '||nfilename
			if revision & nrev \= '?' then
				files = files||' '||nrev
		end
	end
	if date \= '' then do
		call print_log changelog, files, log
		call lineout changelog, ''
	end
	call stream changelog, 'c', 'close'
	if stream(changelog, 'c', 'query size') = 0 then
		call SysFileDelete translate(changelog, '\', '/')
	return

print_log: procedure expose (globals) indent_string
	parse arg changelog, files, log
	do forever
		c = left(log, 1)
		if c \= '(' & c \= '[' then leave
		c = translate(c, ')]', '([')
		i = verify(log, SOH||c, 'm')
		if substr(log, i, 1) \= c then leave
		files = files substr(log, 1, i)
		log = substr(log, i+verify(substr(log, i+1), ' '||TAB))
	end
	call charout changelog, indent_string||'*'||files||':'
	indent = ' '
	if line_indent+1+length(files)+2+pos(SOH, log) >= line_length then do
		call lineout changelog, ''
		indent = indent_string
	end
	do forever
		i = pos(SOH, log)
		if i = 0 then leave
		logline = substr(log, 1, i-1)
		if space(translate(logline, ' ', TAB), 0) = '' then
			call lineout changelog, ''
		else
			call lineout changelog, indent||logline
		log = substr(log, i+1)
		indent = indent_string
	end
	return

read_entries: procedure expose (globals) entries.
	parse arg dir
	filename = dir||'CVS/Entries'
	if stream(filename, 'c', 'open read') \= 'READY:' then
		call die '"'||dir||'" is not a CVS controlled directory.'
	entries. = ''; entries.0 = 0
	do while lines(filename) > 0
		line = linein(filename)
		parse var line 'D/' dir '/'
		if dir \= '' then do
			i = entries.0+1
			entries.i = dir
			entries.0 = i
		end
	end
	call stream filename, 'c', 'close'
	return

parse_log: procedure expose (globals) dateval text.
	parse arg dir, filelist, repository
	files = ''; author. = FALSE
	do i = 1 to words(filelist)
		file = word(filelist, i)
		if file = '*' then
			file = left(dir, length(dir)-1)
		else
			file = dir||file
		if length(files)+length(file) > 1000 then do
			call parse_log_partial files, repository
			files = ''
		end
		files = files file
	end
	if files \= '' then
		call parse_log_partial files, repository
	return

parse_log_partial: procedure expose (globals) dateval text.
	parse arg files, repository
	queue = rxqueue('create')
	call rxqueue 'set', queue
	'@cvs -Qn log -l'||dateval||files||' | rxqueue '||queue
	state = 0
	do while queued() > 0
		line = linein('queue:')
		if state = 0 then do
			if pos('RCS file:', line) = 1 then do
				parse var line . ': ' filename
				if abbrev(filename, repository||'/') then
					filename = substr(filename, length(repository)+2)
				if right(filename, 2) = ',v' then
					filename = left(filename, length(filename)-2)
				i = lastpos('Attic/', filename)
				if i > 0 then
					filename = delstr(filename, i, 6)
				if right(filename, length(changelog_name)) \=,
						changelog_name then do
					rev = '?'
					state = 1
				end
			end
		end
		else if pos('==========', line) = 1 then do
			if state = 2 then call push_text text
			state = 0
		end
		else if state = 1 then do
			if pos('revision ', line) = 1 then do
				parse var line . ' ' rev ' ' .
				state = 2
			end
		end
		else if state = 2 then do
			if pos('----------', line) = 1 then do
				call push_text text
				state = 1
				iterate
			end
			if pos('date: ', line) = 1 then do
				parse var line . ': ' date ' ' time,
					';' . 'author: ' author ';'
				date = translate(date, '-', '/')
				text = filename rev date time author||SOH
				rev = '?'
				iterate
			end
			if pos('branches: ', line) = 1 then iterate
			if line = 'Initial revision' then
				line = 'New file.'
			else do
				parse var line 'file ' .,
					' was initially added on branch ' branch '.'
				if branch \= '' then
					line = 'New file.'
			end
			text = text||line||SOH
		end
	end
	call rxqueue 'delete', queue
	return

push_text: procedure expose text.
	parse arg text
	i = text.0+1
	text.i = text
	text.0 = i
	return
	
sort_log: procedure expose text.
	if text.0 > 1 then
		call quicksort 1, text.0
	return

quicksort: procedure expose text.
	parse arg l, r
	i = l; j = r; x = (l+r)%2; x = text.x
	do forever
		do while compare_log(text.i, x) < 0; i = i+1; end
		do while compare_log(x, text.j) < 0; j = j-1; end
		if i <= j then do
			w = text.i; text.i = text.j; text.j = w
			i = i+1; j = j-1
		end
		if i > j then leave
	end
	if l < j then call quicksort l, j
	if i < r then call quicksort i, r
	return

compare_log: procedure
	parse arg a, b
	parse var a filename1 rev1 date1 time1 author1 log1
	parse var b filename2 rev2 date2 time2 author2 log2
	select
		when date1 time1 << date2 time2 then return +1
		when date1 time1 >> date2 time2 then return -1
		otherwise nop
	end
	select
		when author1 << author2 then return -1
		when author1 >> author2 then return +1
		otherwise nop
	end
	select
		when log1 << log2 then return -1
		when log1 >> log2 then return +1
		otherwise nop
	end
	select
		when filename1 rev1 << filename2 rev2 then return -1
		when filename1 rev1 >> filename2 rev2 then return +1
		otherwise nop
	end
	return 0

is_absolute: procedure expose (globals)
	parse arg path
	return pos(':/', path) = 2 | pos('/', path) = 1

get_repository: procedure expose (globals)
	parse arg dir
	repository = linein(dir||'CVS/Repository')
	call stream dir||'CVS/Repository', 'c', 'close'
	cvsroot = linein(dir||'CVS/Root')
	call stream dir||'CVS/Root', 'c', 'close'
	if repository = '' || cvsroot = '' then
		call die 'This is not a CVS controlled directory.'
	if left(cvsroot, 1) = ':' then
		parse var cvsroot ':' method ':' cvsroot
	else if pos(cvsroot, ':') = 0 then
		method = 'local'
	else
		method = 'server'
	if method = 'local' then do
		if \is_absolute(repository) then
			repository = cvsroot||'/'||repository
		if \is_directory(repository) then
			call die repository||': Bad repository (see CVS/Repository).'
	end
	else
		parse var cvsroot . ':' cvsroot
	return repository

get_hostname: procedure expose (globals)
	hostname = value('HOSTNAME',, 'OS2ENVIRONMENT')
	if hostname \= '' then return hostname
	queue = rxqueue('create')
	call rxqueue 'set', queue
	'hostname | rxqueue '||queue
	if rc = 0 then if lines('queue:') > 0 then
		hostname = linein('queue:')
	call rxqueue 'delete', queue
	if hostname = '' then
		hostname = 'localhost'
	return hostname

find_authors: procedure expose (globals)
	parse arg env
	dir = value(env,, 'OS2ENVIRONMENT')
	if dir \= '' then if exists(dir||'/.cvsauthors') then
		return dir||'/.cvsauthors'
	return ''

read_authors: procedure expose (globals)
	fullname. = ''; mailaddr. = ''
	filename = cvsauthors
	if filename = '' then do
		filename = find_authors('HOME')
		if filename = '' then
			filename = find_authors('ETC')
		if filename = '' then
			return
	end
	if stream(filename, 'c', 'open read') \= 'READY:' then
		call die 'Cannot open' filename 'for reading.'
	do while lines(filename) > 0
		line = linein(filename)
		parse var line login '|' fullname '|' mailaddr
		if login \= '#' then do
			fullname.login = fullname
			mailaddr.login = mailaddr
		end
	end
	call stream filename, 'c', 'close'
	return

die: procedure expose (globals)
	parse arg text
	call lineout 'stderr:', argv.0||': '||text
	exit EXIT_FAILURE

usage: procedure expose (globals)
	say 'Usage: '||argv.0||' [-?Rgv] [-c changelog] [-d date] [-i indent]'
	say '  [-l length] [-t tabwidth] [-a authors] [files...]'
	exit EXIT_USAGE

numeric_argument: procedure expose (globals) optopt optarg
	parse arg minval, maxval
	if \datatype(optarg, 'w') then
		call die '-'||optopt optarg||': invalid argument.'
	if optarg < minval | optarg > maxval then
		call die '-'||optopt optarg||': argument out of range',
			'['||minval||', '||maxval||'].'
	return optarg

getopt: procedure expose (globals) optind optarg optopt optptr
	parse arg options
	if optind = 0 then optptr = 0
	if optptr = 0 | optptr > length(argv.optind) then do
		if optind >= argc then return -1
		optind = optind+1
		optptr = 1
		if substr(argv.optind, optptr, 1) \= '-' then return 0
		optptr = optptr+1
	end
	optopt = substr(argv.optind, optptr, 1)
	optptr = optptr+1
	if optopt = '-' then do
		optind = optind+1
		optptr = 0
		return -1
	end
	i = pos(optopt, options)
	if optopt = ':' | i = 0 then do
		say argv.0||': -'||optopt||' is not a valid option.'
		return '?'
	end
	if substr(options, i+1, 1) = ':' then do
		if optptr <= length(argv.optind) then do
			optarg = substr(argv.optind, optptr)
			optptr = 0
			return optopt;
		end
		if substr(options, i+2, 1) = ':' then do
			i = optind+1
			optptr = 1
			if i < argc & substr(argv.i, optptr, 1) \= '-' then do
				optind = i
				optarg = argv.optind
				optptr = 0
				return optopt
			end
			say argv.0||': -'||optopt||' is missing an argument.'
			return ':'
		end
		optptr = 0
	end
	optarg = ''
	return optopt

setargv: procedure expose (globals)
	parse arg args
	inquote = FALSE
	do forever
		parse var args arg args
		if arg = '' then leave
		quotes = FALSE
		i = 1
		do forever
			i = pos('"', arg, i)
			if i = 0 then leave
			if i > 1 then if substr(arg, i-1, 1) = '\' then do
				arg = delstr(arg, i-1, 1)
				iterate
			end
			arg = delstr(arg, i, 1)
			quotes = \quotes
		end
		if inquote then
			argv.argc = argv.argc arg
		else do
			argv.argc = arg
			argc = argc+1
		end
		if quotes then inquote = \inquote
	end
	return

signal_handler:
	call lineout 'stderr:', argv.0||': terminated by SIGINT.'
	exit EXIT_SIGNAL

